Aller au contenu

Chaîne CI/CD (Forgejo + Woodpecker)

Le homelab héberge sa propre forge et sa propre intégration continue, en logiciels libres, sur deux CT LXC du VLAN 10. C'est ce qui héberge les dépôts (infra, docs…) et vérifie chaque push — y compris celui de ce portail.

Rôle Machine Adresse Logiciel
Forge Git CT 110 forgejo (2 vCPU / 2 Go, disque 16 Go) https://forge.leonheu.fr (public, via Traefik/Coolify) · interne http://10.9.73.81:3000 · SSH 2222 (interne) Forgejo 15.0.4
CI CT 111 ci (4 vCPU / 3 Go, disque 24 Go) http://10.9.73.82:8000 Woodpecker 3.16.0

Les deux CT sont créés par OpenTofu (opentofu/ct-forgejo-ci.tf) et configurés par Ansible (ansible/playbooks/forgejo.yml, ci.yml). LXC Debian 12 non privilégiés ; le CT ci a l'option nesting activée pour faire tourner Docker (backend des étapes de pipeline).

Le flux

  git push ──▶ Forgejo (10.9.73.81)
                  │  webhook (ALLOWED_HOST_LIST = 10.9.73.82)
                  ▼
              Woodpecker server (10.9.73.82:8000)
                  │  gRPC :9000
                  ▼
              Woodpecker agent ──▶ étapes en conteneurs Docker
                  │
                  ▼  statut du build renvoyé
              Forgejo (coche verte / croix rouge sur le commit)
  1. Un push (ou une PR) sur un dépôt Forgejo déclenche un webhook vers Woodpecker. Forgejo bloque par défaut les webhooks vers les IP privées : l'IP du CI est explicitement autorisée (ALLOWED_HOST_LIST = 10.9.73.82 dans app.ini).
  2. Le server Woodpecker lit le fichier .woodpecker/*.yaml du dépôt et planifie les étapes.
  3. L'agent (même CT) exécute chaque étape dans un conteneur Docker (WOODPECKER_BACKEND=docker, 2 workflows en parallèle au plus).
  4. Le résultat est renvoyé à Forgejo et s'affiche sur le commit / la PR.

L'authentification de Woodpecker se fait via Forgejo (OAuth2, WOODPECKER_FORGEJO=true) : on se connecte au CI avec son compte de la forge. L'inscription ouverte est désactivée des deux côtés (DISABLE_REGISTRATION, WOODPECKER_OPEN=false).

Ce qui tourne dessus

  • infra — pipeline .woodpecker/ci.yaml : tofu fmt -check, tofu validate, ansible-lint (non bloquant pour l'instant). Le tofu plan en CI viendra quand la clé age sera disponible sur le runner.
  • docs (ce portail) — mkdocs build --strict : un lien mort fait échouer le build. Le déploiement est ensuite pris en charge par Coolify (cf. Ce site).

Persistance et secrets

  • Données : chacun stocke en SQLite local (/var/lib/forgejo/data, /var/lib/woodpecker). Ces CT sont donc sauvegardés par le vzdump quotidien comme les autres.
  • Secrets (mot de passe admin Forgejo, clés OAuth, AGENT_SECRET…) : chiffrés avec sops dans secrets/services.enc.env, injectés au moment de jouer les playbooks (sops exec-env). Voir le runbook Secrets.

Miroirs externes (GitHub)

Filet de secours hors site : chaque dépôt a un push mirror Forgejo → github.com/leonheu (dépôts privés), synchronisé à chaque push (+ toutes les 8 h). Si le homelab disparaît, le code — y compris les secrets, chiffrés par sops — survit.

  • Config : dépôt Forgejo → Paramètres → Dépôt distant (ou API push_mirrors). Identifiants : token GitHub fine-grained limité aux deux dépôts (permission Contents: RW), archivé dans sops (GITHUB_MIRROR_TOKEN, expire : à renouveler ~juillet 2027).
  • Restauration depuis le miroir : git clone https://github.com/leonheu/<repo> puis recréer le dépôt Forgejo et pousser.

2FA et API Forgejo

Depuis l'activation de la 2FA, l'API Forgejo refuse l'authentification par mot de passe : utiliser un jeton d'accès (Paramètres → Applications). Le jeton d'automatisation courant est archivé dans sops (FORGEJO_API_TOKEN, scope repository uniquement).

Exploitation

Les services sont des unités systemd (binaires, pas de conteneur pour la forge et le server/agent) :

# Sur le CT forgejo (10.9.73.81)
systemctl status forgejo
journalctl -u forgejo -f

# Sur le CT ci (10.9.73.82)
systemctl status woodpecker-server woodpecker-agent docker
journalctl -u woodpecker-agent -f

Rejouer la configuration (idempotent) sur dev-laravel — depuis le dossier ansible/ pour qu'ansible.cfg et l'inventaire soient chargés :

cd ~/projets/infra/ansible
sops exec-env ../secrets/services.enc.env 'ansible-playbook playbooks/forgejo.yml'   # ou ci.yml

Mettre à jour Forgejo ou Woodpecker

Changer forgejo_version / woodpecker_version en tête du playbook, puis rejouer : Ansible télécharge le nouveau binaire et redémarre le service. Faire un test de restauration du CT avant une montée de version majeure de Forgejo (migrations de schéma SQLite).

Déploiement continu (CD) — Coolify

La CI (Woodpecker) vérifie le code ; le déploiement est assuré par Coolify (VM 101, 10.9.73.90). C'est la dernière étape du pipeline qui le déclenche : sur un push main, si les étapes de vérification sont vertes, l'étape deploy appelle l'API de déploiement Coolify. Un build rouge = pas de déploiement.

git push (main) ─▶ Woodpecker : vérifications (ex. mkdocs build --strict)
                        │ vert ?
                        ▼
                   étape deploy ──▶ POST API Coolify ──▶ redéploiement

Brancher une nouvelle app en CD

  1. Créer l'app dans Coolify (build pack, branche main, domaine, clé de déploiement ajoutée côté Forgejo). Noter son uuid (visible dans l'URL de l'app ou son Deploy Webhook).
  2. Token API Coolify : en créer un seul, partagé par toutes les apps (Coolify → Keys & Tokens, permission deploy). Pas besoin d'un token par app — c'est l'uuid qui désigne l'app.
  3. Secret Woodpecker : dépôt → Settings → Secretscoolify_token = le token, événement Push uniquement.
  4. Étape deploy à la fin du .woodpecker/ci.yaml du dépôt :

    deploy:
      image: curlimages/curl:8.11.1
      environment:
        COOLIFY_TOKEN:
          from_secret: coolify_token
      commands:
        - 'curl -fsS -X POST -H "Authorization: Bearer $COOLIFY_TOKEN" "http://10.9.73.90:8000/api/v1/deploy?uuid=<uuid-app>&force=false"'
      when:
        - event: push
          branch: main
    
  5. Push sur main → le pipeline doit finir vert et l'app se redéployer (visible dans Deployments côté Coolify).

Piège vécu

Viser l'IP interne 10.9.73.90:8000, pas le domaine public coolify.leonheu.fr : ce dernier résout vers l'IP publique — un aller-retour par Internet pour deux machines du même VLAN, et un point de défaillance de plus (DNS dynamique).

Webhook « source » sans token ?

Coolify expose aussi /webhooks/source/gitea/events/manual (validé par un secret, sans token API). Plus élégant en théorie, mais il renvoie un 500 « Undefined variable $branch » sur la version actuelle — d'où le choix de l'API + bearer. À retenter après une montée de version de Coolify.