Tous les projets
Projet perso

Déploiement automatisé de ce portfolio (CI/CD)

Comment ce portfolio Astro (rendu serveur, métriques live du homelab) est construit, scanné puis déployé automatiquement : GitLab CI/CD, Buildah, Trivy, Komodo et Traefik, le tout auto-hébergé.

Sommaire 13 sections

Introduction

Pour présenter mes projets et compétences, je voulais un site simple à maintenir, intégré dans une logique DevOps et sécurisé de bout en bout. Ce portfolio est construit avec Astro et tourne sur mon infrastructure personnelle. Il n’est plus un simple site statique : la page d’accueil est rendue côté serveur et affiche des métriques en temps réel de mon homelab.

🔗 Dépôt GitLab : gitlab.ggcorp.it/GGauzins/portfolio

Architecture de l’application

  • Astro 7 en mode serveur (adaptateur @astrojs/node) : la page d’accueil et l’API /api/metrics.json sont générées à chaque requête, les autres pages (CV, projets, 404) sont pré-générées au build.
  • Métriques live : le serveur interroge l’API de Proxmox VE (VMs actives, charge CPU, RAM) et celle de TrueNAS (pools ZFS, CPU, RAM via l’API JSON-RPC en WebSocket). Le navigateur rafraîchit les tuiles toutes les 10 secondes.
  • Sécurité des accès : les jetons API sont en lecture seule (rôle PVEAuditor sur Proxmox, Readonly Admin sur TrueNAS) et restent côté serveur, jamais envoyés au navigateur. Un cache court et un timeout protègent le rendu si le homelab ne répond pas.
  • Articles en Markdown : chaque projet est un fichier .md dont l’en-tête (titre, catégorie, tags, dates) est validé par un schéma au build. Un article affiche sa date de publication et, s’il a été revu, sa date de mise à jour. Les deux sont aussi déclarées aux moteurs de recherche et aux réseaux sociaux (article:published_time, article:modified_time).
  • Aucun framework front : CSS pur et quelques lignes de JavaScript, pour un site léger.
  • Résilience des métriques : si Proxmox ou TrueNAS ne répond pas, le serveur renvoie les dernières valeurs connues, et sans configuration les tuiles gardent des valeurs fixes. Un mode démo génère des valeurs réalistes pour travailler sans le homelab.

Côté site : design et expérience

  • Design « NOC / Blueprint » : l’accueil est pensé comme un écran de supervision, avec un tableau de bord en tuiles (profil, CPU, RAM, VMs, stockage), un fond en grille façon plan technique et des indicateurs « live » qui s’allument quand les données arrivent.
  • Thème Auto / Clair / Sombre : par défaut, le site suit le réglage du système et change en direct si celui-ci change. Le choix est appliqué avant l’affichage de la page, pour éviter tout flash de la mauvaise couleur.
  • Navigation : une barre flottante et translucide, avec la page courante mise en évidence. Sur mobile, les liens passent dans un menu qui se ferme au clic extérieur ou avec Échap, et les tuiles se réorganisent sur une colonne.
  • Projets : un carrousel 3D sur l’accueil, et une page dédiée avec des filtres par catégorie (projets perso, projets BTS).
  • Lecture des articles : un sommaire est généré automatiquement à partir des titres. Sur grand écran, il reste affiché dans une colonne à droite et surligne la section en cours de lecture ; sur mobile, il devient un bloc repliable. Il passe sur deux niveaux quand l’article a des sous-sections.
  • Contenu centralisé : expériences, formations et compétences sont écrites une seule fois, dans des fichiers de données, puis reprises à la fois par l’accueil et par la page CV. Une mise à jour se fait donc à un seul endroit.
  • Référencement : sitemap généré au build, URL canoniques, balises Open Graph pour les aperçus sur LinkedIn et les autres réseaux, et robots.txt.
  • Accessibilité : lien d’accès direct au contenu pour la navigation au clavier, textes alternatifs sur les images, et animations désactivées quand le système demande moins de mouvement.

Pourquoi GitLab (auto-hébergé) ?

  • Self-hosting : GitLab tourne sur mes propres serveurs, avec un contrôle total sur l’infra, la sécurité et les données.
  • Tout au même endroit : code, pipelines, registre d’images et scans de sécurité.
  • Workflow structuré : je développe sur la branche dev, et la production (main) n’est mise à jour qu’après validation, via une merge request.
  • Règles documentées : les conventions du projet (rédaction des articles, anonymisation des extraits d’infra, circuit de publication) sont écrites dans le dépôt, dans docs/CONVENTIONS.md.

Stratégie de déploiement : Docker + Komodo

Mon idée de départ était GitLab Pages, mais l’usage d’un domaine personnalisé (www.ggauzins.fr) imposait une 2ᵉ IP publique sur le serveur GitLab, incompatible avec mon infra. J’ai donc opté pour une solution Dockerisée :

  • GitLab CI construit une image Docker contenant le serveur Node.
  • L’image est poussée dans le GitLab Container Registry, puis scannée.
  • Sur une VM de production, le conteneur tourne dans une stack Komodo, l’outil qui pilote tous mes hôtes Docker. Son option d’auto-update surveille l’image :latest et redéploie automatiquement chaque nouvelle image validée.
  • Traefik expose le site en HTTPS.

Résultat : un git merge vers main suffit à mettre en production, sans jamais déployer une image non vérifiée.

Pipeline GitLab CI/CD (DevSecOps)

Le pipeline est découpé en quatre étapes. Le build du site et le scan de l’image sont bloquants : en cas d’échec, rien ne part en production. Les autres scanners avertissent sans bloquer, pour signaler un problème sans figer les livraisons.

Étape Jobs Rôle
test build, npm_audit, sbom, trivy_config, SAST, secrets Qualité et sécurité du code
build_image build_image Image Buildah taguée :<sha>
scan trivy_image Scan Trivy bloquant
release release Tag :latest, sur main seulement
  • test : vérification des types (astro check) et build du site, audit des dépendances npm, inventaire SBOM CycloneDX, analyse du Dockerfile par Trivy, analyse statique (Semgrep) et détection de secrets.
  • build_image : l’image est construite avec Buildah et poussée sous un tag immuable, le hash du commit.
  • scan : Trivy analyse l’image (système et dépendances). Une vulnérabilité HIGH ou CRITICAL corrigeable arrête le pipeline.
  • release : uniquement sur main, l’image scannée est retaguée en :latest (et :<version>), ce qui déclenche le déploiement.

Sur la branche dev, le pipeline va jusqu’au scan de l’image sans la publier : toute la chaîne est testée avant le merge, sans toucher à la production.

Quelques choix techniques :

  • Buildah plutôt que Docker-in-Docker ou Kaniko : pas de démon Docker ni de conteneur privilégié, et Kaniko n’est plus maintenu depuis 2025.
  • Tag immuable par commit : chaque version reste disponible dans le registre, ce qui permet un rollback immédiat.
  • Images CI figées sur une version précise (Trivy, cdxgen, Buildah, regctl), pour des builds reproductibles et moins exposés à la supply chain.
  • Dépendances à jour : l’audit npm a par exemple révélé une faille critique dans Astro 6 (exécution de code à distance via l’optimisation d’images). Le passage à Astro 7 l’a corrigée, et npm audit ne remonte plus aucune vulnérabilité. Avant chaque montée de version, le rendu des pages est comparé avant et après.

Extrait de la chaîne image :

build_image:
  stage: build_image
  image: quay.io/buildah/stable:v1.43.4
  variables:
    STORAGE_DRIVER: vfs
    BUILDAH_FORMAT: docker
    BUILDAH_ISOLATION: chroot
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - buildah build --file Dockerfile --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - buildah push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

trivy_image:
  stage: scan
  image:
    name: aquasec/trivy:0.74.0
    entrypoint: [""]
  script:
    - trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

release:
  stage: release
  image:
    name: ghcr.io/regclient/regctl:v0.11.6-alpine
    entrypoint: [""]
  script:
    - echo "$CI_REGISTRY_PASSWORD" | regctl registry login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --pass-stdin
    - regctl image copy "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

Dockerfile (multi-stage, non-root)

# --- Étape 1 : build (avec toutes les dépendances) ---
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- Étape 2 : runtime léger, non-root ---
FROM node:22-alpine AS runtime
ENV NODE_ENV=production HOST=0.0.0.0 PORT=8080
WORKDIR /app
COPY package*.json ./
# Dépendances de production uniquement, puis retrait de npm (inutile au runtime)
RUN npm ci --omit=dev \
 && npm cache clean --force \
 && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
 && chown -R node:node /app
COPY --chown=node:node --from=builder /app/dist ./dist
USER node
EXPOSE 8080
CMD ["node", "./dist/server/entry.mjs"]

L’image finale ne contient que Node, les dépendances de production et le site compilé. Elle tourne avec un utilisateur non privilégié, et npm est retiré pour réduire la surface d’attaque.

Reverse proxy Traefik

http:
  routers:
    portfolio:
      entryPoints: [websecure]
      rule: "Host(`www.ggauzins.fr`)"
      tls:
        certResolver: lets-encr
      service: portfolio
    redirect-naked:
      entryPoints: [websecure]
      rule: "Host(`ggauzins.fr`)"
      middlewares: [redirect-to-www]
      service: noop@internal
      tls:
        certResolver: lets-encr
  middlewares:
    redirect-to-www:
      redirectRegex:
        regex: "^https?://ggauzins\\.fr/(.*)"
        replacement: "https://www.ggauzins.fr/$1"
        permanent: true
  services:
    portfolio:
      loadBalancer:
        servers:
          - url: "http://<ip-vm-prod>:8086/"

Les deux domaines (www. et nu) sont servis en HTTPS via Let’s Encrypt (challenge DNS OVH), et ggauzins.fr redirige vers www.ggauzins.fr.

Stack Komodo (VM de prod)

Le déploiement est une stack Komodo, dont voici le docker-compose.yml :

services:
  portfolio:
    image: registry.ggcorp.it/ggauzins/portfolio:latest
    container_name: portfolio
    restart: unless-stopped
    env_file: .env
    ports:
      - "8086:8080"

Et l’environnement de la stack, qui devient le .env du conteneur :

# --- Proxmox VE : jeton API, rôle PVEAuditor (lecture seule) ---
PVE_URL=https://<ip-proxmox>:8006
PVE_TOKEN=[[PVE_TOKEN]]

# --- TrueNAS : clé API, rôle Readonly Admin (lecture seule) ---
TRUENAS_URL=https://<ip-truenas>
TRUENAS_KEY=[[TRUENAS_KEY]]

# --- Pools ZFS affichés sur la page d'accueil ---
TN_POOL_HDD=<pool-hdd>
TN_POOL_NVME=<pool-nvme>
  • Mise à jour automatique : l’auto-update de la stack détecte chaque nouvelle image :latest publiée par le job release et redéploie le conteneur, en environ une minute.
  • Secrets hors de Git : le fichier .env (URLs et jetons API Proxmox / TrueNAS en lecture seule) est généré par Komodo au déploiement. Les jetons y sont stockés comme variables secrètes et injectés par référence (PVE_TOKEN=[[PVE_TOKEN]]) : ils n’apparaissent ni dans le dépôt, ni dans la définition de la stack.
  • Rollback : il suffit de remplacer :latest par le tag :<sha> voulu dans la stack et de redéployer.

Maintenance du registre

Une règle de nettoyage GitLab tourne chaque semaine : elle conserve latest, les numéros de version et les 10 builds les plus récents, et supprime les tags de plus de 30 jours au-delà. Le registre ne grossit pas indéfiniment, tout en gardant de quoi revenir en arrière.

Technologies utilisées

  • Astro : framework web, en rendu serveur (Node)
  • GitLab CI/CD et Container Registry : pipeline et stockage des images
  • Buildah : construction d’images sans démon
  • Trivy : scan des vulnérabilités et des mauvaises configurations
  • cdxgen : SBOM CycloneDX
  • Semgrep (SAST GitLab) et détection de secrets
  • Komodo : gestion des stacks Docker, secrets et mise à jour automatique du conteneur
  • Traefik : reverse proxy et certificats Let’s Encrypt
  • Proxmox VE et TrueNAS : sources des métriques live

Résultat

  • Site en ligne en HTTPS : www.ggauzins.fr, avec métriques du homelab en temps réel
  • Déploiement 100 % automatisé : un merge sur main, et la production est à jour en quelques minutes
  • Aucune image non scannée ne peut atteindre la production
  • Chaque version conservée et restaurable en une ligne
  • Infrastructure et domaine entièrement maîtrisés

Conclusion

Ce projet met en pratique une démarche DevSecOps complète : intégration continue avec contrôles qualité, build d’image sans privilège, scans de sécurité bloquants, livraison continue, reverse proxy et gestion des certificats. Le tout appliqué à une application Astro en rendu serveur, branchée sur mon homelab.