Je mets en place la construction et le déploiement automatique de ce blog Hugo avec Woodpecker et Gitea.
Information
Update du 2025-02-02
Modifications afin de fonctionner sous la version 3.0 de Woodpecker
Information
Update du 2024-09-01
Depuis la publication de cet article, la syntaxe a un peu changé.
Cet article tien maintenant compte de ces changements.
Préambule
Ce blog (construit grâce au framwork Hugo)
est de base, compiler et publier manuellement.
Il est aussi versionnalisé sous le SCM
Git (j’utilise la forge Gitea).
De ce fait, le processus d’ajout d’un article implique les étapes suivantes :
- Commit dans Gitea
- Compilation via la commande
hugo
- Déploiement via une commande
rsync
Bien que j’ai automatisé les deux dernières parties dans un script,
je ne suis pas satisfait de cela car je sais qu’il existe des solutions
pour faire cela automatiquement.
Cela était donc un bon prétexte pour expérimenter le
CI/CD.
Si vous lisez cet article, c’est que ma solution de
déploiement automatique a fonctionné !
Nous allons donc voir ici ma démarche et comme d’habitude,
les problèmes que j’ai eus… 😒
Introduction
Choix de la solution
Sur la recherche d’une solution, mes exigences sont les suivantes :
- Self-hosted (non Cloud)
- Léger
- Déploiement via Docker
- Documentation efficace
- Solution moderne
Dans le cas d’un système de CI, je souhaite aussi :
- Utilisation de conteneurs
- Intégration avec Gitea
Mes recherches mon orienté vers Jenkins
et DroneCI qui,
tous les deux, permettait l’utilisation de Gitea.
J’ai testé Jenkins, mais je trouve cela trop compliqué de le câbler
pour une utilisation avec des conteneurs.
Au premier abord, j’ai été séduit par Drone, mais je n’ai pas compris
grand-chose à la documentation… De plus, je n’ai pas trouvé
beaucoup de ressources externes sur cette solution.
Récemment (mais je ne sais déjà plus comment), j’ai découvert
WoodpeckerCI qui est un fork de Drone.
La documentation est assez épurée et compréhensible,
j’ai donc décidé de lui donner une chance.
Principe
Woodpecker utilise les comptes de votre forge pour fonctionner.
Il n’y a alors pas de configuration particulière à faire si ce
n’est l’interconnexion avec votre forge.
On va donc faire le nécessaire pour pouvoir s’authentifier
grâce à l’Open Authorization 2.0
(OAuth2).
Une fois cela configuré, on ajoute notre dépôt à Woodpecker
ce qui a pour conséquence de créer un Webhook dans le projet
afin que ce dernier soit notifié lors d’évènements (push
, pull request
, …)
et puisse effectuer des actions en conséquence.
Ces actions, définit dans un pipeline au sein du dépôt
sont exécutées selon des contraintes qu’on lui applique.
Sans configuration particulière, le pipeline est lancé
à chaque push dans le dépôt.
Hypothèses
Dans cet article, j’utiliserais les valeurs suivantes :
- URL de Gitea :
https://ennuyeux.mondomaine.mu
- Utilisateur admin de Gitea :
anup
- URL (local) de Woodpecker :
http://192.168.17.4:8000
- URL (publique) de Woodpecker :
https://pivert.mondomaine.mu
Mise en place
Gitea OAuth2
Pour générer les informations d’authentification, il faut aller dans la section Applications des paramètres de notre utilisateur (soit le lien suivant : ennuyeux.mondomaine.mu/user/settings/applications).
Dans la section Manage OAuth2 Applications, on renseigne donc nos informations :
Le Redirect URI doit obligatoirement prendre la forme
<URL de Woodpecker>/authorize
comme le précise la
documentation.
Ainsi, on obtient alors les informations dont on aura besoin pour la suite :
On relève donc les informations Client ID
et Client Secret
pour après.
Woodpecker
Sur mon serveur qui va héberger Woodpecker, je crée donc le dossier qui va accueillir la configuration en Docker Compose.
Fichier compose.yml
:
---
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: unless-stopped
environment:
- WOODPECKER_OPEN=${MY_WOODPECKER_OPEN}
- WOODPECKER_HOST=${MY_WOODPECKER_HOST}
- WOODPECKER_AGENT_SECRET=${MY_WOODPECKER_AGENT_SECRET}
- WOODPECKER_GITEA=${MY_WOODPECKER_GITEA}
- WOODPECKER_GITEA_URL=${MY_WOODPECKER_GITEA_URL}
- WOODPECKER_GITEA_CLIENT=${MY_WOODPECKER_GITEA_CLIENT}
- WOODPECKER_GITEA_SECRET=${MY_WOODPECKER_GITEA_SECRET}
- WOODPECKER_ADMIN=${MY_WOODPECKER_ADMIN}
- WOODPECKER_FORGE_TIMEOUT=${MY_WOODPECKER_FORGE_TIMEOUT}
- WOODPECKER_FORGE_RETRY=${MY_WOODPECKER_FORGE_RETRY}
ports:
- 8000:8000
volumes:
- /var/lib/woodpecker-server/:/var/lib/woodpecker/
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: unless-stopped
depends_on:
- woodpecker-server
command: agent
volumes:
- /etc/woodpecker-agent/:/etc/woodpecker/
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=${MY_WOODPECKER_AGENT_SECRET}
Avec cette façon de faire, je crée un fichier .env
,
au même endroit que le fichier Docker Compose.
Ce dernier sera alors chargé lors du docker compose up -d
.
Fichier .env
:
MY_WOODPECKER_OPEN=True
MY_WOODPECKER_HOST=https://pivert.mondomaine.mu
MY_WOODPECKER_AGENT_SECRET=01cafe02cafe03cafe04cafe05cafe06cafe07cafe08cafe09cafe10cafecafe
MY_WOODPECKER_GITEA=True
MY_WOODPECKER_GITEA_URL=https://ennuyeux.mondomaine.mu
MY_WOODPECKER_GITEA_CLIENT=a0a0a0a0-1111-2222-3333-b4b4b4b4b4b4
MY_WOODPECKER_GITEA_SECRET=gto_azertyuiop789qsdfghjklm456wxcvbn123azertyuiop789qsdf
MY_WOODPECKER_ADMIN=anup
MY_WOODPECKER_FORGE_TIMEOUT=60s
MY_WOODPECKER_FORGE_RETRY=5
Les deux conteneurs que compose la stack doivent se partager
un secret. J’associe donc la variable MY_WOODPECKER_AGENT_SECRET
avec le résultat de la commande openssl rand -hex 32
.
On renseigne ainsi les valeurs de MY_WOODPECKER_GITEA_CLIENT
et MY_WOODPECKER_GITEA_SECRET
avec les valeurs respectives de
Client ID
et Client Secret
obtenue sur Gitea.
Il est important de préciser la liste des utilisateurs admin
de votre forge, car dans le cas contraire, certains paramètres
ne seront pas paramétrables.
Si vous avez plusieurs admin, séparer les noms dans
la variable MY_WOODPECKER_ADMIN
par des virgules.
Reverse Proxy (Traefik)
Ce serveur étant dans mon LAN, j’ajoute un fichier de configuration dynamique pour Traefik :
Fichier traefik/config/dynamique/woodpecker.yml
---
# Woodpecker CI
http:
routers:
woodpecker-server:
rule: Host(`pivert.mondomaine.mu`)
entrypoints:
- superoueb
service: woodpecker-server
tls:
certResolver: tomandjerry
services:
woodpecker-server:
loadBalancer:
servers:
- url: 'http://192.168.17.4:8000'
Cela permet donc à mon serveur Gitea (dans le cloud), d’accéder
à mon serveur Woopecker en HTTPS.
Je ne suis pas fou, je n’ai pas ouvert le 8000 sur ma box !
Vous remarquerez que la variable MY_WOODPECKER_HOST
tiens compte de cela.
Configuration
Activation des dépôts
Une fois logué sur Woodpecker via notre compte Gitea,
on peut activer les dépôts sur lequel on voudra
bénéficier de Woodpecker via le bouton Add repository.
Ainsi, on active ceux que l’on veut en cliquant sur Enable.
Vous pourrez alors constater qu’un Webhook a été crée sur les dépôts activés (dans votre projet Gitea : Settings > Webhooks).
Création de pipeline
Pour créer un pipeline, il suffit de créer un
fichier .woodpecker.yaml
à la racine de
votre dépôt.
Vous pouvez aussi créer
plusieurs pipeline
dans un dossier selon le format .woodpecker/*.yml
.
Application
Clone
La première action lancée lors du lancement
d’un pipeline et le clonage complet du dépôt.
Cette action est implicite, il n’y a pas
besoin de la déclarer.
Moi, mon clonage ne fonctionnait pas !
Pour vous expliquer pourquoi, je résume si dessous
un exemple des commandes qui sont effectués lors du clonage.
git init -b master
git remote add origin https://ennuyeux.mondomaine.mu/Scrample/Blog.git
git fetch --no-tags --depth=1 --filter=tree:0 origin +refs/heads/master:
git reset --hard -q a727a45843fe0463bb9c9c8de55da137fb420a0b
git submodule update --init --recursive
git lfs fetch
git lfs checkout
Le dépôt de mon blog contient mon thème en tant que submodule :
Mon fichier .gitmodules
:
[submodule "themes/myLoveIt"]
path = themes/myLoveIt
url = ssh://git@ennuyeux.mondomaine.mu/Scrample/myLoveIt.git
Le problème est que ce submodule est déclaré en SSH alors que Woodpecker utilise le HTTP pour cloner.
Pour résoudre cela, deux options s’offrent à moi :
- Passer le submodule en HTTP sur le dépôt
- Surcharger la configuration Woodpecker pour l’utiliser HTTP
La première solution ne me plait pas car je trouve cela plus simple de gérer l’authentification via SSH.
Ayant choisi la deuxième solution, j’ai donc du explicité le pipeline de clonage :
clone:
- name: git
image: woodpeckerci/plugin-git
settings:
lfs: true
recursive: true
submodule_override:
themes/myLoveIt: https://ennuyeux.mondomaine.mu/Scrample/myLoveIt.git
branch: master
Cela veut donc dire qu’au moment où Woodpecker
initie la tache de clonage, il met en cache le
nouveau lien du submodule afin d’utiliser celui-ci
en lieu et place de celui préciser dans le fichier .gitmodules
.
Build
L’étape de construction nécessite un conteneur
sur lequel hugo
y est installé.
Je n’ai pas réussi à faire fonctionner le plugin Drone Hugo, j’ai donc utilisé le conteneur d’un utilisateur d’Hugo qui est cité dans la documentation d’Hugo.
Précisément, j’utilise la version spéciale CI : klakegg/hugo:ext-alpine-ci
Mon début de pipeline commence donc ainsi :
steps:
- name: build
image: klakegg/hugo:ext-alpine-ci
Vous vous attendiez à quelque chose de plus long ? 😜
Deploy
Rsync
Pour la partie déploiement, cela se complique un peu.
Je fais un rsync
via SSH vers le dossier qui est
exposé dans la configuration de mon conteneur Nginx.
Une fois de plus, j’ai essayé le
plugin Drone rsync
sans succès.
De ce que j’ai diagnostiqué, c’est l’utilisation de la clé privée
qui pose problème. Après de nombreux tests infructueux,
je me suis donc résolue à faire mon propre conteneur pour cela.
J’ai alors créé un Dockerfile dans mon dépôt dont le contenu est le suivant :
Fichier .docker/rsync_deploy/Dockerfile
:
FROM alpine:latest
RUN apk --update add --no-cache openssh-client rsync
De ce fait, j’ai deux actions à créer :
- Construire l’image
- Effectuer le déploiement
# steps:
- name: build-deploy-image
image: docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- cd .docker/rsync_deploy; docker build -t internal/rsync_deploy:latest .
# steps:
- name: deploy
image: internal/rsync_deploy:latest
environment:
SSH_KEY_BASE64:
from_secret: ssh_key_base64
commands:
- mkdir /root/.ssh
- chmod 700 /root/.ssh
- echo "$SSH_KEY_BASE64" | base64 -d >/root/.ssh/id_ecdsa
- chmod 600 /root/.ssh/id_ecdsa
- rsync -az -e 'ssh -p 22 -i /root/.ssh/id_ecdsa -o UserKnownHostsFile=/dev/null -o LogLevel=quiet -o StrictHostKeyChecking=no' public/ root@web.mondomaine.mu:/var/www/html/
Hugo publie le site construit dans le dossier public
.
C’est donc ce dossier que je copie sur mon serveur Web.
Ajustements
Afin que cela puisse fonctionner, il faut faire deux choses sur le projet :
- Autoriser les pipelines à monter des volumes
- Ajouter la clé privée de manière confidentielle
Sur l’interface de Woodpecker, dans les paramètres du projet, on coche la case Trusted pour pouvoir monter des volumes (notamment le socket Docker pour pouvoir créer une image).
Pour la clé privée SSH, j’ai créé un secret dans le projet. Afin de garantir que la clé passe bien dans la variable, je l’ai convertie en base64 via la commande :
cat ~/.ssh/id_ecdsa | base64
Ainsi, je l’ajoute sur le projet :
Remarque
Ne vous inquiétez pas, la clé de cette copie d’écran est une fausse que j’ai créé depuis IT-TOOLS. 😉
Notify
Pour la notification, comme d’habitude,
j’utilise Gotify.
Je me suis donc appliqué à faire une notification avec
un lien cliquable qui me renvoie sur la page de mon projet.
Pour cette solution, j’utilise le plugin Drone Webhook.
Afin de gérer la notion de succès et d’échec du pipeline,
je crée deux actions dont une où l’exécution est forcée
en cas d’échec.
On est obligé de faire cela, car si une des actions échoue,
le pipeline s’arrête (et donc les actions suivantes ne sont pas lancées).
- name: notify-on-success
image: plugins/webhook
settings:
urls:
from_secret: gotify_url_tokenized
content_type: application/json
template: |
{
"extras": {
"client::display": {
"contentType": "text/markdown"
}
},
"title": "Building : ${CI_REPO}",
"message": "Succes on build [${CI_BUILD_NUMBER}](${CI_BUILD_LINK}) for commit ${CI_COMMIT_SHA}",
"priority": 5
}
- name: notify-on-failure
image: plugins/webhook
settings:
urls:
from_secret: gotify_url_tokenized
content_type: application/json
template: |
{
"extras": {
"client::display": {
"contentType": "text/markdown"
}
},
"title": "Building : ${CI_REPO}",
"message": "Failed on build [${CI_BUILD_NUMBER}](${CI_BUILD_LINK}) for commit ${CI_COMMIT_SHA}",
"priority": 5
}
when:
status:
- failure
J’utilise ici aussi un secret (gotify_url_tokenized
)
qui contient l’URL de Gotify avec le token :
TL;DR
Voici le pipeline au complet :
clone:
- name: git
image: woodpeckerci/plugin-git
settings:
lfs: true
recursive: true
submodule_override:
themes/myLoveIt: https://ennuyeux.mondomaine.mu/Scrample/myLoveIt.git
branch: master
steps:
- name: build-deploy-image
image: docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- cd .docker/rsync_deploy; docker build -t internal/rsync_deploy:latest .
- name: build
image: klakegg/hugo:ext-alpine-ci
- name: deploy
image: internal/rsync_deploy:latest
environment:
SSH_KEY_BASE64:
from_secret: ssh_key_base64
commands:
- mkdir /root/.ssh
- chmod 700 /root/.ssh
- echo "$SSH_KEY_BASE64" | base64 -d >/root/.ssh/id_ecdsa
- chmod 600 /root/.ssh/id_ecdsa
- rsync -az -e 'ssh -p 22 -i /root/.ssh/id_ecdsa -o UserKnownHostsFile=/dev/null -o LogLevel=quiet -o StrictHostKeyChecking=no' public/ root@web.mondomaine.mu:/var/www/html/
- name: notify-on-success
image: plugins/webhook
settings:
urls:
from_secret: gotify_url_tokenized
content_type: application/json
template: |
{
"extras": {
"client::display": {
"contentType": "text/markdown"
}
},
"title": "Building : ${CI_REPO}",
"message": "Succes on build [${CI_BUILD_NUMBER}](${CI_BUILD_LINK}) for commit ${CI_COMMIT_SHA}",
"priority": 5
}
- name: notify-on-failure
image: plugins/webhook
settings:
urls:
from_secret: gotify_url_tokenized
content_type: application/json
template: |
{
"extras": {
"client::display": {
"contentType": "text/markdown"
}
},
"title": "Building : ${CI_REPO}",
"message": "Failed on build [${CI_BUILD_NUMBER}](${CI_BUILD_LINK}) for commit ${CI_COMMIT_SHA}",
"priority": 5
}
when:
status:
- failure
Utilisation
On push alors nos modifications et on admire le lancement du pipeline :
Ainsi que la notification sur Gotify :
On voit donc ici que, après la récéption du push, la construction prend au total 31 secondes.
Conclusion
Avant de mettre en place cela, mon script de déploiement
engageait mon terminal pendant une vingtaine de seconde.
C’est donc le temps que j’ai gagné grâce à cet outil !
Plus sérieusement, cela m’a permis de comprendre
les notions de CI/CD.
Les problèmes que j’ai rencontrés ont eu un impact
positif car cela m’a permis de faire mon premier
Dockerfile
et d’utiliser les
Messages Extras de Gotify.
En espérant que cet article puisse aider ceux qui hésite à se lancer dans l’aventure. Bon courage à tous !