Bilan d’une plateforme data multi-partenaires : IaC, sécurité et DX

devops
nomad
keycloak
vault
iac
security
fastapi
retour-experience
Author

Sylvain Pham

Published

February 15, 2026

Retour d’expérience sur la conception et l’exploitation d’une plateforme data distribuée pour un projet multi-partenaires international. Plusieurs sites distants, des sources de données hétérogènes, un cluster Nomad, et zéro Kubernetes.

Le contexte

Un projet collaboratif international avec plusieurs partenaires répartis en Europe. Chacun produit des données IoT (capteurs, compteurs, équipements) qu’il faut agréger, stocker, visualiser et exposer via une API sécurisée.

Le cahier des charges implicite :

  • Infrastructure reproductible (IaC), opérable avec une équipe réduite
  • Multi-tenancy stricte : chaque partenaire ne voit que ses données
  • SSO pour les humains, tokens API pour les machines
  • Budget cloud limité (4 VMs Azure en cluster, pas de managed services)
  • Pas d’équipe ops dédiée — un DevOps fait le gros du travail

Le projet était coordonné par des chefs de projet. Côté technique, l’essentiel de l’infra, la sécurité, le dev et le CI/CD a été porté par un profil DevOps unique.

Le résultat : 33 jobs Nomad, 4 microservices FastAPI, un portail partenaire, une status page, et un pipeline CI/CD complet. Le tout tourne sur un cluster de 4 VMs.

L’infrastructure : Nomad comme colonne vertébrale

Pourquoi pas Kubernetes ?

La question revient systématiquement. Avec 4 VMs et un seul opérateur technique, Kubernetes aurait été un frein :

Critère Kubernetes Nomad
Complexité minimale etcd + API server + scheduler + kubelet Un seul binaire
Courbe d’apprentissage Steep (CRDs, operators, RBAC) HCL lisible, deploy en 5 min
Ressources requises ~2 GB RAM pour le control plane ~100 MB
Jobs batch/cron CronJob (verbeux) type = "batch" + periodic
Multi-driver Containers only (sans KubeVirt) Docker, exec, raw_exec, Java…

Nomad s’est révélé idéal pour ce profil : peu de ressources, déploiement rapide, HCL expressif.

Organisation des namespaces

infra/     → traefik, keycloak, vault, apisix, etcd, oauth2-proxy
prod/      → api, portail, ingestion (x3), status-page
default/   → registry-cleanup (batch), csi-azurefiles (plugin)

Point d’attention : les Nomad Variables sont scopées par namespace. Déplacer un job d’un namespace à un autre sans migrer ses variables = secrets invisibles au runtime. Ça m’a coûté 2h de debug.

Traefik : le routeur central

Toute la plateforme passe par un seul Traefik v3.3 en reverse proxy. Chaque job Nomad déclare ses labels Traefik directement dans le HCL :

service {
  tags = [
    "traefik.enable=true",
    "traefik.http.routers.api.rule=Host(`api.platform.example.eu`)",
    "traefik.http.routers.api.tls.certresolver=letsencrypt",
  ]
}

Un seul point d’entrée, des certificats Let’s Encrypt automatiques, zéro configuration nginx.

La sécurité : couche par couche

Gestion des secrets : Vault + Workload Identity

Chaque job Nomad reçoit un JWT signé par le serveur Nomad. Ce token est échangé automatiquement contre un token Vault scopé au chemin kv/data/<nom-du-job> :

sequenceDiagram
    participant Job as Job Nomad
    participant Nomad as Nomad Server
    participant Vault as Vault
    Job->>Nomad: Démarre
    Nomad->>Job: JWT signé
    Job->>Vault: Échange JWT
    Vault->>Job: Token Vault (read-only, TTL court)

Aucun secret en dur, ni dans les jobs HCL, ni dans le CI. Les credentials des APIs externes (partenaires, sources de données) vivent dans Nomad Variables ou dans Vault.

task "ingestion" {
  vault {}

  template {
    data = <<EOF
{{ with secret "kv/data/ingestion-service" }}
API_KEY={{ .Data.data.API_KEY }}
API_SECRET={{ .Data.data.API_SECRET }}
{{ end }}
EOF
    destination = "secrets/env.env"
    env         = true
  }
}

Identité : Keycloak multi-realm

Trois realms Keycloak pour trois populations distinctes :

Realm Population Usage
staff Équipe interne SSO Grafana, Nomad UI, InfluxDB, OAuth2-Proxy
partners Partenaires externes Portail self-service, API tokens
m2m Services techniques Gateway API (APISIX), ingestion services

Le SSO staff couvre tous les outils internes : un seul login, même session, MFA activé.

Pour les partenaires, un mécanisme d’identity brokering permet à un utilisateur du realm partners de se connecter à Grafana via le realm staff, sans duplication de compte. Le flux :

flowchart LR
    A[Partenaire] --> B[Realm partners]
    B --> C[IdP Broker]
    C --> D[Realm staff]
    D --> E[Grafana<br/>Viewer]

Chaque partenaire est automatiquement assigné à un groupe Grafana correspondant à son site (folder-level permissions).

Gateway API : APISIX devant l’API publique

L’API data est exposée via APISIX avec authentification key-auth :

flowchart LR
    A[Client] --> B[APISIX<br/>key-auth]
    B --> C[API upstream]
    C --> D[(InfluxDB)]
    C --> E[(PostgreSQL)]

Chaque partenaire reçoit un consumer APISIX avec une clé API unique. Le portail permet de générer/révoquer ces clés en self-service. Rate limiting et logging inclus.

Piège rencontré : APISIX limite le champ desc à 256 caractères. Pour stocker des métadonnées sur les consumers, il faut utiliser le champ labels (key-value libre).

Accès SSH : Zero-Trust avec Teleport

Pas de clé SSH partagée, pas de bastion classique. Teleport gère l’accès aux VMs :

  • Certificats SSH éphémères (TTL 8h)
  • RBAC par rôle (admin, audit, dev)
  • Audit log de chaque session
  • Compatible VSCode Remote-SSH
# Se connecter
tsh login --proxy=access.platform.example.eu
tsh ssh user@vm-server

Scan de vulnérabilités

Chaque image Docker est scannée par Trivy dans le pipeline CI :

trivy-scan:
  script:
    - trivy image --severity HIGH,CRITICAL --exit-code 0 $IMAGE
  allow_failure: true

Non-bloquant pour l’instant (les CVEs kernel dans linux-libc-dev sont inévitables avec Debian), mais visible dans chaque pipeline.

Réseau : WireGuard pour les sites distants

Les sites distants se connectent au cluster central via un hub WireGuard. Chaque site a un tunnel chiffré point-à-point. Le cluster central ne route que le trafic nécessaire (pas de full mesh).

Le développement : microservices FastAPI

Architecture applicative

Quatre microservices Python indépendants, chacun avec son Dockerfile et son pipeline :

flowchart TD
    T[Traefik<br/>reverse proxy] --> API[API centrale<br/>FastAPI]
    T --> I1[Ingestion Site 1<br/>FastAPI]
    T --> I2[Ingestion Site 2<br/>FastAPI]
    T --> I3[Ingestion Site 3<br/>FastAPI]
    API --> DB1[(InfluxDB<br/>time-series)]
    API --> DB2[(PostgreSQL<br/>données métier)]
    I1 --> DB1
    I2 --> DB1
    I3 --> DB1

L’API centrale agrège les requêtes vers InfluxDB (données capteurs) et PostgreSQL (données métier). Chaque service d’ingestion gère un site partenaire avec des méthodes d’intégration différentes :

Service Source Méthode Fréquence
Ingestion Site 1 API REST partenaire Polling HTTP Manuel / ponctuel
Ingestion Site 2 Dashboard Grafana tiers Scraping API Cron quotidien (6h)
Ingestion Site 3 Fichiers JSON embarqués Chargement batch Manuel

Trois partenaires, trois méthodes d’intégration complètement différentes. C’est la réalité des projets multi-partenaires.

Base de données : stockage polyglotte

  • InfluxDB : un bucket par site partenaire, requêtes Flux pour l’agrégation temporelle
  • PostgreSQL : schéma métier (15 tables, 2 vues matérialisées pour KPIs)
  • Neo4j : graphe de relations (expérimental)

Le schéma PostgreSQL modélise un domaine métier transactionnel : participants, offres, échanges, et facturation.

Tests

Smoke tests pour chaque service (pytest + httpx), exécutés dans le container Docker pendant le CI :

test-api:
  script:
    - docker run --rm $IMAGE pytest test_smoke.py -v

Plus des tests d’intégration en Hurl (HTTP request language) pour valider les endpoints en production :

GET https://api.platform.example.eu/docs
HTTP 200

GET https://api.platform.example.eu/data/site1/
HTTP 200
[Asserts]
jsonpath "$.status" == "ok"

L’intégration App ↔︎ Infra : là où ça devient intéressant

CI/CD : GitLab → Docker → Nomad

Le pipeline est simple et linéaire :

flowchart LR
    A[git push] --> B[Build images]
    B --> C[Smoke tests]
    C --> D[Trivy scan]
    D --> E[Deploy Nomad]
    E --> F[Trigger ingestion]

Le runner GitLab est un shell executor directement sur le serveur Nomad. Pas de Docker-in-Docker, pas de Kubernetes executor. docker build et nomad job run sont des commandes locales.

deploy-api:
  stage: deploy
  script:
    - nomad job run -namespace=prod nomad/api.nomad.hcl
  when: manual

29 jobs CI au total : 4 builds, 3 tests, 4 scans Trivy, 9 deploys infra, 5 deploys apps, 4 triggers d’ingestion.

Le portail partenaire : self-service complet

Stack : FastAPI + Jinja2 + HTMX (pas de framework JS lourd)

Le portail permet aux partenaires de :

  • Se connecter via SSO Keycloak
  • Générer/révoquer leurs clés API (via l’Admin API APISIX)
  • Consulter la documentation de l’API
  • Accéder à leurs dashboards Grafana (identity brokering)

Chaque action sur le portail se traduit par un appel à l’API Admin APISIX. Pas de base de données propre au portail — APISIX est la source de vérité pour les consumers et les clés.

La status page : monitoring visible

Stack : FastAPI + HTMX + Jinja2 + PostgreSQL (Neon, hébergé externe)

Vérification en deux couches :

  1. Interne : ping direct sur le port du service (santé du container)
  2. Publique : requête HTTP via Traefik avec le bon Host header (santé du routage)

Si le container répond mais que Traefik ne route pas → état “dégradé” (point orange). Ça a sauvé plusieurs fois un diagnostic rapide.

Documentation as Code

Un site MkDocs déployé automatiquement via le pipeline CI :

build-docs:
  rules:
    - changes:
        - docs/**/*
        - mkdocs.yml

160+ pages de documentation : architecture, guides par rôle (dev, data scientist, ops), journal opérationnel quotidien, procédures de recovery.

Taskfile : 100+ commandes opérationnelles

Un Taskfile.yml centralise toutes les opérations récurrentes :

task teleport:users          # Lister les utilisateurs SSH
task teleport:create -- alice admin  # Créer un accès
task dev:check-vpn           # Vérifier la connectivité VPN
task api:test                # Lancer les tests d'intégration

C’est un gain DX énorme : un nouveau contributeur peut opérer la plateforme sans connaître les commandes sous-jacentes.

Les chiffres

Métrique Valeur
Jobs Nomad 33
Services applicatifs 4 microservices + portail + status page
Pipeline CI 29 jobs (build, test, scan, deploy, trigger)
Scripts opérationnels 24
Dashboards Grafana 24 (infra + par site partenaire)
Tests (smoke + intégration) 20 smoke + 16 suites Hurl
Pages de documentation 160+
Commandes Taskfile 100+
VMs en cluster 4
DevOps (infra + sec + dev) 1

Ce que j’en retiens

Nomad, c’est suffisant

Pour une plateforme avec 33 jobs, des cron batches, du service discovery, et des secrets Vault, Nomad fait le travail sans le overhead de Kubernetes. Le HCL est lisible, le deploy est rapide, et le debugging (logs, allocs, events) est direct.

Le multi-tenancy est un problème d’identité, pas d’infrastructure

Trois realms Keycloak + APISIX consumers + Grafana folder permissions = isolation complète des partenaires. Pas besoin de namespaces Kubernetes ou de network policies complexes. L’identité porte l’isolation.

L’intégration app-infra change tout pour la DX

Quand le DevOps qui sécurise l’infra est aussi celui qui écrit le code applicatif et le pipeline CI, les frictions disparaissent. Le template Vault dans le HCL, les labels Traefik dans le service, le nomad job run dans le CI — tout est au même endroit, dans le même langage de pensée.

HTMX > React pour les outils internes

Le portail et la status page sont en HTMX + Jinja2. Pas de build JS, pas de node_modules, pas de state management. Pour des outils internes avec peu d’interactivité, c’est un gain de productivité et de maintenance considérable.

La documentation, c’est de l’infra

160 pages de docs maintenues comme du code (MkDocs + CI auto-deploy). Le journal opérationnel quotidien a sauvé des heures de debug à chaque incident. “Qu’est-ce qu’on avait changé hier ?” → git log docs/journal/.

Les galères (pour être honnête)

Infra / Cloud

  • Azure Load Balancer : après la migration HA du cluster Nomad, l’IP publique restait attachée à une seule VM. Si ce nœud tombait, Traefik pouvait redémarrer ailleurs mais le trafic externe n’arrivait plus. Il a fallu créer un Standard LB Azure avec tous les nœuds en backend pool. Le genre de SPOF qu’on ne voit qu’en test de failover.
  • Nomad HA migration : promouvoir 2 clients en serveurs a fait tomber 5 jobs d’ingestion en dead — ils avaient épuisé leurs restart attempts pendant les reboots. Cluster HA, mais workloads crashés. Re-soumission manuelle de chaque job.
  • Docker cross-node : Docker 29 avec userland-proxy casse le trafic inter-hosts. Fix : "userland-proxy": false dans daemon.json. 4h de debug aveugle, le symptôme ressemblait à un problème NSG.

Sécurité / Identité

  • Teleport TLS : 4 tentatives pour faire marcher tsh en CLI — TLS routing, Let’s Encrypt, network mode host. Finalement résolu avec le port 443 et l’auth OIDC Keycloak, mais ça a pris du temps avant de trouver la bonne combinaison.
  • Grafana SSO realm : les URLs OAuth pointaient vers le realm master au lieu du realm applicatif. Erreur cryptique (“Invalid username or password”) qui ne pointait pas du tout vers la cause réelle. 10 minutes de fix, 2h pour comprendre.
  • Identity brokering partenaires : après activation du broker Keycloak, conflits “email already exists” dans Grafana — les anciens comptes créés par OAuth2-Proxy avaient un auth_id différent. A nécessité ALLOW_INSECURE_EMAIL_LOOKUP + suppression du middleware OAuth2-Proxy sur la route Grafana.

Dev / Config

  • APISIX : la galère la plus longue (~3 jours cumulés). Format YAML changé entre versions, { env } vide dans le template, collision de port avec docker-proxy, noms de consumers avec tirets invalides, erreurs de redirect HTTPS en chaîne. Chaque fix en révélait un autre.
  • Nomad Variables + namespaces : déplacer un job sans ses variables = crash silencieux. Toujours vérifier le scope.
  • sed -i sur des bind mounts Docker : crée un nouvel inode, le container voit encore l’ancien fichier. Ne jamais éditer un fichier monté en volume avec sed -i.
  • Trivy + Debian : build-essential tire linux-libc-dev avec 27 CVEs kernel. Purger après le build.

Conclusion

Ce projet montre qu’un DevOps avec des compétences transverses — sécurité, automatisation, développement — peut concevoir et livrer une plateforme data multi-tenant avec des partenaires distribués. Sans Kubernetes, sans managed services coûteux, et sans équipe technique dédiée.

La coordination était assurée par des chefs de projet. Le fait qu’un même profil porte l’IaC, la sécurité et le code applicatif réduit les frictions — mais ça demande aussi d’accepter les compromis quand on est le seul à arbitrer.

Les clés : Nomad pour l’orchestration, Keycloak pour l’identité, Vault pour les secrets, Traefik pour le routage, et beaucoup de documentation.

L’ensemble a fait l’objet d’une première démo concluante avec les partenaires — portail, dashboards, API, ingestion en conditions réelles. La stack tient.

C’est pas parfait. Les tests pourraient couvrir plus, et le monitoring mériterait Prometheus + alerting. Mais pour un projet avec des contraintes réelles, ça tient la route.


Conçu et opéré depuis début 2026. Stack testée en conditions réelles avec plusieurs partenaires internationaux.