Blog

Concevoir sa propre plateforme CI/CD : retour d'expérience

Piloter une dizaine de dépôts et une vingtaine d'applications, bibliothèques et sites web pose vite une question simple mais structurante : industrialise-t-on la chaîne de build et de déploiement, ou continue-t-on à la bricoler dépôt par dépôt ? Le choix fait ici a été de concevoir une plateforme CI/CD auto-hébergée, pensée comme un produit interne à part entière — avec ses propres règles de conception, sa documentation générée automatiquement et son cycle de vie.

Précision importante avant d'entrer dans le détail : cette plateforme n'a pas vocation à faire « mieux » qu'une offre SaaS établie (GitHub Actions, GitLab CI, Bitbucket Pipelines...) sur le plan des fonctionnalités ou du confort d'exploitation. C'est un choix de souveraineté — garder la main sur le code, les artefacts et les données de build, chez soi — assumé avec ses coûts, et pertinent avant tout pour une structure qui dispose déjà d'une capacité IT interne pour l'opérer dans la durée. Ce point est développé plus bas.

Cet article ne détaille pas de configuration précise : il décrit les décisions d'architecture et les raisons qui les sous-tendent.

Vue d'ensemble

flowchart TB
    DEV((Développeur)) -->|push / pull requests| GITEA

    subgraph HOST["Infrastructure conteneurisée (hôte unique)"]
        direction TB
        NGINX["Nginx — reverse proxy"]

        subgraph CICD["Stack cicd"]
            direction LR
            GITEA["Gitea
forge Git"] WOODP["Woodpecker
serveur CI/CD"] AGENT["Woodpecker
agent"] NEXUS["Nexus
artefacts binaires"] VERD["Verdaccio
registre npm"] MINIO["Minio
stockage objet"] PG[("Postgres")] end subgraph ADMIN["Stack admin"] PORTAINER["Portainer"] end subgraph ACME["Stack acme"] ACMESH["acme.sh
certificats TLS"] end NGINX --> GITEA NGINX --> WOODP NGINX --> NEXUS NGINX --> VERD NGINX --> MINIO GITEA -->|webhook| WOODP WOODP --> AGENT WOODP --> PG AGENT --> NEXUS AGENT --> VERD AGENT --> MINIO ACMESH -.certificats.-> NGINX PORTAINER -.supervision.-> CICD end

Trois piles logiques (cicd, admin, acme), un seul socle de templates, un seul hôte capable de porter l'ensemble — le tout généré depuis une configuration Python centralisée plutôt qu'assemblé à la main.

Une plateforme auto-hébergée, pas subie

La chaîne repose sur des briques open source assemblées et opérées en propre plutôt que sur des services SaaS : forge Git, moteur CI, dépôt d'artefacts binaires, registre de paquets, stockage objet, base relationnelle et reverse proxy tournent tous en conteneurs sur une infrastructure maîtrisée de bout en bout. Ce choix a un coût — l'exploitation est à la charge de l'équipe — mais il donne un contrôle total sur la disponibilité, la rétention des artefacts et la localisation des données, pour un volume de projets suffisamment important pour justifier l'investissement.

Une infrastructure volontairement légère

À aucun moment le dimensionnement de la plateforme ne dépasse le besoin réel. Le choix des briques va systématiquement vers l'outil le plus sobre capable de tenir la charge : une forge Git mono-binaire plutôt qu'une suite DevOps complète, un moteur CI agent-based au format YAML natif plutôt qu'un orchestrateur historique à forte empreinte, un registre npm minimaliste plutôt qu'un registre d'entreprise généraliste. Pas d'orchestrateur de conteneurs pour trois piles de composition Docker — la complexité d'un Kubernetes n'aurait rien apporté à cette échelle, seulement des couches d'abstraction à opérer.

Cette légèreté se traduit jusque dans l'exploitation quotidienne : la totalité de la chaîne (forge, CI, artefacts, registre, stockage objet, base de données, reverse proxy) tient sur un seul hôte, avec un jeu d'images essentiellement basées sur des variantes minimales (alpine, slim) pour les étapes de pipeline. C'est aussi ce qui rend crédible un hôte qui se met en veille : une infrastructure taillée large n'aurait pas cette latitude.

L'infrastructure comme code, sans usine à gaz

Plutôt que de dupliquer des fichiers docker-compose.yml pour chaque environnement, la configuration entière repose sur un moteur de templates maison : les fichiers de composition sont générés à partir de gabarits paramétrés, fusionnés par un outil dédié, et alimentés par une source de vérité unique écrite en Python. Chaque valeur (port, chemin de volume, domaine) n'existe qu'à un seul endroit ; tout le reste — services d'intégration continue, portail d'administration, renouvellement de certificats — en dérive. L'objectif n'était pas d'adopter un outil d'infrastructure-as-code généraliste, mais de construire l'outil minimal correspondant exactement au besoin, avec un formalisme volontairement homogène d'un fichier de configuration à l'autre.

Une convention qui élimine une classe entière de bugs

Une règle simple structure tout le système de ports : le port interne d'un conteneur et le port exposé sur l'hôte ne sont jamais identiques. Ce choix, qui peut sembler anodin, supprime en pratique une source récurrente de confusion lors des diagnostics et des évolutions de configuration — on ne se demande jamais si un numéro de port fait référence au monde interne ou externe. C'est le genre de convention qui ne coûte rien à poser tôt et qui coûte cher à retrofitter plus tard.

Deux pipelines, une seule vérité

Le build et le déploiement sont volontairement découplés en deux pipelines distincts plutôt qu'un seul flux monolithique :

  • Le pipeline de build se déclenche sur chaque push : il nettoie l'espace de travail, dérive automatiquement la version sémantique à partir du message de commit, compile, empaquette, publie l'artefact binaire dans le dépôt d'artefacts, puis tague le commit correspondant.
  • Le pipeline de déploiement se déclenche indépendamment, sur un événement de déploiement explicite ou manuel. Il ne rebuild jamais rien : il va chercher l'artefact déjà publié et le pousse vers la cible.

Le lien entre les deux est assuré par un objet de métadonnées transitant par le stockage objet, qui garantit qu'un déploiement correspond exactement à un build identifié — pas de dérive possible entre ce qui a été testé et ce qui est mis en production. Le transfert vers l'hôte cible et l'exécution du script de déploiement distant sont eux-mêmes scriptés de bout en bout, sans étape manuelle.

Cette séparation permet aussi de rejouer un déploiement sans reconstruire, et de garder une frontière nette entre « ce qui produit un artefact » et « ce qui l'installe ».

sequenceDiagram
    participant Dev as Développeur
    participant Gitea
    participant Build as Woodpecker (pipeline build)
    participant Nexus
    participant Minio
    participant Deploy as Woodpecker (pipeline deploy)
    participant Cible as Serveur cible

    Dev->>Gitea: push sur main
    Gitea->>Build: webhook
    Build->>Build: version dérivée du commit
    Build->>Nexus: publication de l'artefact versionné
    Build->>Minio: dépôt des métadonnées de build
    Build->>Gitea: commit + tag

    Note over Dev,Cible: déploiement déclenché indépendamment

    Dev->>Deploy: événement de déploiement
    Deploy->>Minio: lecture des métadonnées de build
    Deploy->>Nexus: récupération de l'artefact exact
    Deploy->>Cible: transfert de l'artefact
    Deploy->>Cible: exécution du script de déploiement distant

Un seul dépôt, plusieurs modules déployables

Chaque projet déclare explicitement les modules qu'il expose au déploiement plutôt que de coupler artificiellement une application à une seule cible. Cette indirection permet à un même mécanisme générique de servir aussi bien une application isolée qu'un ensemble de services livrés ensemble, sans dupliquer la logique de pipeline pour chaque cas.

Documentation vivante plutôt que documentation maintenue à la main

Un point de conception notable : les diagrammes d'architecture (dépendances entre services, réseaux, volumes) ne sont pas dessinés à la main puis oubliés — ils sont générés directement à partir des fichiers de composition Docker, avec une syntaxe de diagramme standard. La documentation d'architecture ne peut donc pas diverger silencieusement de la réalité opérationnelle : elle est régénérée à la demande depuis la source de vérité.

Sobriété opérationnelle

L'infrastructure intègre des mécanismes d'exploitation qui dépassent le simple « ça tourne » :

  • des services système arrêtent et relancent proprement les piles de conteneurs autour des cycles de veille de l'hôte, pour ne pas laisser tourner inutilement ce qui peut être mis en pause ;
  • un mécanisme de surveillance périodique redémarre automatiquement l'agent d'intégration continue s'il devient indisponible, sans intervention humaine.

Ce sont des détails, mais ce sont précisément les détails qui distinguent une plateforme pensée pour durer d'un empilement d'outils fonctionnel le jour du déploiement initial seulement.

Secrets et accès

La gestion des secrets (identifiants de registre, clés d'accès, jetons de forge) est isolée de la configuration fonctionnelle plutôt que dispersée dans les fichiers de composition, avec un composant dédié à la gestion centralisée des secrets. Les informations d'authentification sensibles transitent par les mécanismes natifs du moteur CI (secrets chiffrés) plutôt que par des variables en clair dans les pipelines.

Une réflexion déjà engagée sur la suite

Au-delà de l'existant, une spécification a été rédigée pour faire évoluer la gestion de configuration des applications déployées : versionner les configurations elles-mêmes dans Git (une branche par couple application/version), les pousser indépendamment du code applicatif, et déclencher un rechargement à chaud plutôt qu'un redéploiement complet. C'est un exemple de démarche où la conception précède l'implémentation — le problème de fond (dissocier cycle de vie du code et cycle de vie de la configuration, à l'échelle d'une vingtaine d'applications) est posé et résolu sur le papier avant d'être codé.

Pour qui, et pourquoi pas pour tout le monde

Rien de ce qui précède n'est un argument contre les offres SaaS. Pour une petite équipe sans ressource IT dédiée, un GitHub Actions ou un GitLab CI managé reste très probablement le choix le plus rationnel : zéro exploitation, mise à jour automatique, support éditeur. L'auto-hébergement ne rend pas ces avantages caducs, il déplace simplement le curseur — moins de dépendance à un tiers, en échange de plus de responsabilité opérationnelle.

Le profil auquel ce type de plateforme s'adresse est donc précis : une petite entreprise qui dispose déjà d'une équipe ou d'une personne IT capable d'opérer un socle de conteneurs dans la durée (mises à jour de sécurité, sauvegardes, supervision, astreinte), et pour qui la maîtrise du lieu et de la façon dont le code et les artefacts sont hébergés est un critère à part entière — souveraineté des données, indépendance vis-à-vis d'un éditeur Cloud, coût maîtrisé à l'échelle d'une vingtaine d'applications plutôt qu'un abonnement par utilisateur ou par minute de build. Sans cette capacité IT interne, le calcul s'inverse rapidement : le temps d'exploitation coûte plus cher que ce qu'il fait économiser.

Ce que ça montre

À l'échelle d'une dizaine de dépôts et d'une vingtaine d'applications, bibliothèques et sites, la valeur ne vient pas d'un outil miracle mais de l'accumulation de décisions cohérentes : une source de vérité unique pour la configuration, une séparation nette entre construire et déployer, une traçabilité garantie par construction plutôt que par discipline, une documentation qui ne peut pas mentir, et une attention portée à l'exploitation dans la durée autant qu'à la mise en production initiale.

Rien de tout cela ne prétend surpasser une plateforme SaaS sur la richesse fonctionnelle ou le confort d'exploitation — l'objectif était ailleurs : garder la maîtrise complète d'une chaîne de build et de déploiement, pour une structure en mesure d'en assumer l'exploitation.

Traductions: