Aller au contenu

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.created et job.success en 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.