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)
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> :
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-serverScan 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: trueNon-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 -vPlus 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: manual29 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 :
- Interne : ping direct sur le port du service (santé du container)
- Publique : requête HTTP via Traefik avec le bon
Hostheader (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.yml160+ 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égrationC’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-proxycasse le trafic inter-hosts. Fix :"userland-proxy": falsedansdaemon.json. 4h de debug aveugle, le symptôme ressemblait à un problème NSG.
Sécurité / Identité
- Teleport TLS : 4 tentatives pour faire marcher
tshen 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
masterau 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_iddiffé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 -isur des bind mounts Docker : crée un nouvel inode, le container voit encore l’ancien fichier. Ne jamais éditer un fichier monté en volume avecsed -i.- Trivy + Debian :
build-essentialtirelinux-libc-devavec 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.