Cas d'étude — Plateforme d'automatisation¶
Appliquer la validation et la gouvernance a une plateforme d'automatisation infrastructure — collections Ansible, multi-provider, orchestration centralisee.
Contexte¶
Ce second cas d'étude traverse le même parcours d'architecture (chapitres 00 a 08) avec un système fondamentalement différent : une plateforme d'automatisation infrastructure plutôt qu'une application SaaS. Les concepts sont les mêmes, les décisions sont différentes.
Plateforme d'automatisation basée sur les collections Ansible — multi-provider (vSphere, GCP, Azure), multi-environnement (production, staging, dev). L'objectif est de centraliser le provisioning, la configuration et le cycle de vie des ressources infrastructure via un control plane unifie qui orchestre des exécution nodes specialises.
Cadrage — exigences non-fonctionnelles¶
| Attribut | Cible | Mesure |
|---|---|---|
| Temps de provisioning VM | < 15 minutes | Durée job.created a job.success |
| Error rate playbooks | < 1% | Ratio failed / total sur 24h glissantes |
| Disponibilité control plane | 99.9% | Health check externe /healthz |
| Idempotence | 100% des playbooks | Tests d'idempotence en CI |
L'idempotence est l'exigence la plus structurante : un playbook exécuté deux fois doit produire le même résultat. C'est la garantie fondamentale de fiabilité de la plateforme.
ADR structurant¶
ADR-001 : Collections Ansible comme unite de packaging et de versioning¶
Contexte : plusieurs équipes contribuent aux playbooks, plusieurs providers sont cibles, les versions doivent être gérées independamment par domaine fonctionnel.
Décision : chaque domaine (system, network, monitoring, application, database, identity, cloud) est une collection Ansible indépendante avec son propre cycle de versioning semver. Le control plane consomme des versions fixes des collections.
Conséquences : isolation des changements par domaine, possibilite de tester chaque collection independamment, overhead de gestion des versions a absorber.
Gouvernance : le comite d'architecture revoit semestriellement le découpage en collections. Critère de révision : si une collection dépassé 50 rôles ou si le couplage inter-collections augmente, on envisage un re-découpage.
Topologie — vue C4¶
C4Container
title Plateforme d'automatisation — Container diagram
Person(engineer, "Ingenieur infrastructure", "Declenche des jobs, consulte l'etat")
Person(operator, "Operateur", "Supervise les executions, gere les incidents")
System_Boundary(control, "Control Plane") {
Container(api, "API", "Python/FastAPI", "REST API — jobs, inventaires, collections")
Container(scheduler, "Scheduler", "Python/Celery", "Ordonnancement et distribution des jobs")
Container(inventory, "Inventory Manager", "Python", "Gestion des inventaires dynamiques")
ContainerDb(pg, "PostgreSQL", "PostgreSQL 16", "Etat du control plane — jobs, runs, tenants")
}
System_Boundary(collections, "Collections Ansible") {
Container(col_system, "nuevolia.system", "Ansible Collection", "OS hardening, packages, utilisateurs")
Container(col_network, "nuevolia.network", "Ansible Collection", "VLANs, firewall, DNS")
Container(col_monitoring, "nuevolia.monitoring", "Ansible Collection", "Prometheus, Grafana, alerting")
Container(col_app, "nuevolia.application", "Ansible Collection", "Deploy applicatif, conteneurs")
Container(col_db, "nuevolia.database", "Ansible Collection", "PostgreSQL, MySQL, backup")
Container(col_identity, "nuevolia.identity", "Ansible Collection", "LDAP, Keycloak, certificats")
Container(col_cloud, "nuevolia.cloud", "Ansible Collection", "vSphere, GCP, Azure — provisioning VMs")
}
System_Boundary(exec, "Execution Layer") {
Container(runner1, "Execution Node (vSphere)", "Ansible Runner", "Dedie au provider vSphere")
Container(runner2, "Execution Node (GCP)", "Ansible Runner", "Dedie au provider GCP")
Container(runner3, "Execution Node (Azure)", "Ansible Runner", "Dedie au provider Azure")
}
System_Boundary(infra, "Infrastructure transverse") {
Container(vault, "Secret Store", "OpenBao", "Secrets, credentials providers, certificats")
Container(obs, "Observability Stack", "Prometheus / Grafana / Loki", "Metriques, logs, traces d'execution")
}
Rel(engineer, api, "REST HTTPS")
Rel(operator, obs, "HTTPS")
Rel(api, scheduler, "interne")
Rel(api, inventory, "interne")
Rel(api, pg, "SQL")
Rel(scheduler, runner1, "SSH / API Runner")
Rel(scheduler, runner2, "SSH / API Runner")
Rel(scheduler, runner3, "SSH / API Runner")
Rel(runner1, vault, "mTLS")
Rel(runner2, vault, "mTLS")
Rel(runner3, vault, "mTLS")
Rel(runner1, obs, "metriques / logs")
Rel(runner2, obs, "metriques / logs")
Rel(runner3, obs, "metriques / logs") Ce diagramme est maintenu en Structurizr DSL dans l'architecture repository. Les collections sont ajoutees ou retirees via PR avec revue architecturale.
Communication¶
- REST (control plane vers API) : déclenchement de jobs, consultation de l'état, gestion des inventaires. API versionee, documentation OpenAPI générée automatiquement.
- SSH / Ansible Runner API : le scheduler communique avec les exécution nodes via l'API Ansible Runner ou directement par SSH selon le mode de déploiement. Les runners sont stateless — ils reçoivent un job, l'exécutent, retournent le résultat.
- Events (suivi d'exécution) : les exécution nodes emettent des événements de progression (task started, task ok, task failed) que le control plane consomme pour mettre à jour l'état en base et alimenter l'observabilité.
Guardrail : une fitness function vérifié que chaque collection expose ses rôles via une interface documentee (fichier meta/main.yml complet). Pas de rôle sans documentation d'interface.
Données¶
- PostgreSQL (control plane) : état des jobs (pending, running, success, failed), historique des runs, inventaires, configurations des collections par environnement. Source de vérité pour le suivi opérationnel.
- Fact caching (jsonfile) : Ansible cache les facts collectes en début de play dans des fichiers JSON sur les exécution nodes. Cache invalide explicitement avant les runs critiques (provisioning initial, changement de configuration majeur).
- OpenBao : tous les secrets (credentials vSphere, service accounts GCP/Azure, mots de passe bases de données, clés SSH) sont stockes dans OpenBao. Les runners fetchent leurs secrets au moment de l'exécution via le plugin lookup Ansible
hashi_vault.
Gouvernance : la politique de gestion des secrets est documentee dans l'architecture repository. Tout nouveau type de secret passe par une revue sécurité avant d'être ajoute a OpenBao.
Résilience¶
- Idempotence Ansible native : tous les playbooks sont écrits pour être idempotents — une re-exécution sur un système déjà configuré ne produit pas d'effet de bord. C'est la garantie fondamentale de résilience de la plateforme.
- Retry sur les providers cloud : les modules cloud (GCP, Azure, vSphere) implementent un retry avec backoff exponentiel pour les erreurs transitoires d'API (rate limiting, timeout réseau). Configurable par provider.
- Health checks des exécution nodes : le scheduler vérifié la disponibilité des exécution nodes avant distribution. Un node qui ne répond pas est marque indisponible et ses jobs sont redistribues.
- Dead letter pour les jobs en échec : les jobs qui échouent après le nombre maximal de tentatives sont archives avec leur contexte complet pour analyse post-mortem.
Fitness function : un test d'idempotence automatise exécuté chaque playbook deux fois et vérifié que la seconde exécution ne produit aucun changement (changed=0).
Sécurité¶
- mTLS entre control plane et exécution nodes : chaque exécution node possédé un certificat client emis par la PKI interne (géré via nuevolia.identity). Le control plane vérifié le certificat avant d'accepter un callback.
- OpenBao pour les secrets : zero secret en variable d'environnement ou en fichier plat. Les credentials providers sont stockes dans OpenBao avec des leases a durée limitee. Rotation automatique des credentials dynamiques (GCP service accounts, Azure managed identities).
- RBAC par équipe et par environnement : l'API du control plane applique un RBAC a deux dimensions — l'équipe (qui peut déclencher quoi) et l'environnement (production requiert une approbation explicite, staging et dev sont en self-service). Les permissions sont stockees en base et evaluees à chaque requête.
Guardrails sécurité :
- Scan des rôles Ansible pour les anti-patterns sécurité (shell sans
no_log, credentials en clair) - Vérification que tous les modules cloud utilisent le lookup
hashi_vault(pas de credentials en variables) - Audit automatique des leases OpenBao — alerte si un lease dépassé 24h
Validation — SLO et fitness functions¶
SLO définis et instrumentes¶
- Provisioning VM < 15 minutes — mesure du temps entre
job.createdetjob.successen base, histogram Prometheus - Error rate playbooks < 1% — ratio jobs failed / jobs total sur une fenêtre glissante de 24h
- Disponibilité control plane 99.9% — health check externe sur
/healthz, alerte PagerDuty si down
Fitness functions CI¶
# Verifie que les collections n'importent pas de variables d'inventaire
# directement depuis un autre namespace de collection
def test_collection_inventory_separation():
"""
Les collections ne doivent pas acceder aux variables d'inventaire
d'un autre bounded context (cross-collection coupling interdit).
"""
for collection_dir in Path("collections").iterdir():
if not collection_dir.is_dir():
continue
collection_name = collection_dir.name
for task_file in collection_dir.rglob("tasks/*.yml"):
content = task_file.read_text()
for other_collection in Path("collections").iterdir():
if other_collection.name == collection_name:
continue
prefix = other_collection.name.replace(".", "_") + "_"
assert prefix not in content, (
f"{task_file}: la collection '{collection_name}' "
f"reference des variables de "
f"'{other_collection.name}' ({prefix}...)"
)
# Verifie que chaque playbook declare explicitement ses tags
def test_all_playbooks_have_tags():
for playbook in Path("playbooks").rglob("*.yml"):
content = playbook.read_text()
assert "tags:" in content, (
f"{playbook}: le playbook doit declarer des tags "
f"pour permettre l'execution partielle"
)
# Verifie l'idempotence de chaque role
def test_role_idempotence():
"""
Execute le role deux fois et verifie que la seconde
execution ne produit aucun changement.
"""
...
Gouvernance en action¶
Le comite d'architecture de la plateforme revoit trimestriellement :
- Les métriques SLO : temps de provisioning, taux d'échec, disponibilité
- Le découpage en collections : couplage inter-collections, taille des collections
- Le Technology Radar : nouvelles versions Ansible, nouveaux providers, outils a évaluer
- La dette technique : rôles a refactorer, modules deprecies, tests manquants
La guild d'infrastructure se reunit bi-hebdomadairement pour partager les retours d'expérience, présenter les nouveaux rôles, et discuter des patterns emergents.
Tip
La séparation collection/inventaire est l'invariant architectural central de la plateforme. Une collection qui dépend de la structure d'un inventaire spécifique n'est plus réutilisable — elle devient un script deguise. Cette règle est protégée par une fitness function et reviewee à chaque PR.
Dette technique identifiée¶
| Item de dette | Sévérité | Effort | Stratégie |
|---|---|---|---|
| Fact caching en fichiers JSON locaux | Moyenne | Moyen | Migrer vers Redis pour les facts partages |
| Pas de rollback automatise des playbooks | Élevée | Élevé | Concevoir un mécanisme de snapshot/restore |
| Tests d'intégration sur VMs réelles (lents) | Moyenne | Moyen | Introduire Molecule avec des containers |
| Documentation des rôles incomplète | Faible | Faible | Boy scout rule, enrichir à chaque PR |
| Couplage implicite via les group_vars | Élevée | Moyen | Refactorer vers des defaults de collection |
Le couplage via les group_vars est la dette la plus dangereuse : elle contourne la séparation collection/inventaire sans que la fitness function actuelle ne la détecté. Un renforcement de la fitness function est planifie.
Technology Radar de la plateforme¶
| Quadrant | Technologie | Anneau | Justification |
|---|---|---|---|
| Orchestration | Ansible 2.16 | Adopt | Standard, maîtrise par l'équipe |
| Orchestration | Ansible AWX | Hold | Remplacé par le control plane interne |
| Providers | vSphere | Adopt | Provider principal, stable |
| Providers | GCP | Adopt | Provider cloud primaire |
| Providers | Azure | Trial | En cours d'intégration, retours en cours |
| Secrets | OpenBao | Adopt | Fork communautaire HashiCorp Vault, standard interne |
| Observabilité | Prometheus/Grafana | Adopt | Standard métriques et dashboards |
| Observabilité | Loki | Trial | Aggregation logs, en évaluation |
| Testing | Molecule | Assess | Tests de rôles en containers, a experimenter |
Leçons apprises¶
Après 12 mois d'exploitation de la plateforme :
- Les collections Ansible comme bounded contexts fonctionnent. Le versioning indépendant par domaine a permis à l'équipe réseau de livrer 3 fois plus vite sans impacter l'équipe système. Le coût de gestion des versions est réel mais acceptable.
- L'idempotence est le test le plus precieux. Sur 500 rôles, 12 avaient des problèmes d'idempotence non détectés avant l'introduction de la fitness function. Chacun aurait pu causer un incident en production.
- La gouvernance doit être adaptée au contexte. Pour une équipe infrastructure de 5 personnes, un comite mensuel d'une heure et des ADR en PR suffisent. Le processus RFC formel aurait été disproportionne.
- Les guardrails automatises sont plus efficaces que les gates humaines pour ce type de plateforme. La fitness function de séparation collection/inventaire a bloque 8 PRs en 6 mois — chacune aurait introduit du couplage invisible.
Conclusion du parcours¶
Ce cas d'étude clôture un parcours de dix chapitres à travers l'architecture logicielle — des fondations matérielles (chapitre 00) jusqu'à la gouvernance continue (chapitre 09).
Les idées centrales de ce parcours :
- L'architecture est un ensemble de décisions structurantes — pas un diagramme, pas un document, pas un rôle. Ce sont les choix qui coutent cher a changer et qui conditionnent tout le reste.
- Chaque décision est un trade-off — il n'y a pas de bonne architecture dans l'absolu, il y à l'architecture qui répond aux contraintes du moment. Documenter les trade-offs (via les ADR) est aussi important que les faire.
- La validation transforme les intentions en garanties — les fitness functions, les SLO instrumentes, les revues structurées empêchent le système de dériver silencieusement de ses objectifs.
- La gouvernance maintient la cohérence dans la durée — pas par la bureaucratie, mais par des standards clairs, des guardrails automatises, et un processus de décision léger qui permet aux équipes d'avancer en autonomie.
- L'architecture évolue — un système qui ne change pas est un système mort. L'objectif n'est pas de concevoir l'architecture parfaite du premier coup, mais de construire un système capable d'évoluer quand le contexte change.
Le meilleur architecte n'est pas celui qui à la meilleure réponse. C'est celui qui pose les bonnes questions, documente les compromis, et construit les mécanismes qui permettent au système — et à l'équipe — de s'adapter.