Intégration plateforme : control plane et infrastructure
Ce document décrit comment l'infrastructure (deploy/) est alignée sur le code du control plane (services/control-plane/src/eai). Le code backend fait foi : en cas d'écart, c'est le déploiement qui est corrigé. Chaque point est vérifié par un test services/control-plane/tests/test_platform_*.py.
Worker de fond control-plane-worker
python -m eai.worker exécute les tâches périodiques de la plateforme : tick du planificateur, vidage de l'outbox d'événements, expiration des réservations de quota, détection des heartbeats périmés, rétention. Un verrou consultatif PostgreSQL garantit qu'une seule instance est active.
- Helm :
templates/control-plane/worker.yaml, activé parworker.enabled: true(valeur par défaut). Une seule réplique, stratégieRecreate, même image, même ConfigMap, même Secret (External Secrets ou agent OpenBao) et même ServiceAccount que l'API. Pas de PodDisruptionBudget. - NetworkPolicy (Cilium ou Kubernetes) : aucune entrée ; sortie vers PostgreSQL (5432) et NATS (4222) dans le namespace data, OpenBao (8200), le collecteur OTLP/HTTP (4318) et le DNS.
- Compose : service
control-plane-worker, mêmes variables quecontrol-plane(ancre YAMLx-control-plane-env), sans port publié. - Supervision : l'alerte
EaiStaleHeartbeatsse déclenche quandeai_heartbeat_stale_runsreste positif 10 minutes (worker arrêté ou runtimes bloqués).
Métriques Prometheus
GET /metrics (absent de l'OpenAPI) expose le format texte de prometheus_client. Le module eai/metrics.py définit les familles et des fonctions sans risque (aucune exception ne remonte dans la requête) :
| Métrique | Type | Labels | Source |
|---|---|---|---|
eai_http_requests_total | compteur | method, route, status | middleware HTTP |
eai_http_request_duration_seconds | histogramme | method, route | middleware HTTP |
eai_gateway_requests_total | compteur | provider, model, outcome | metrics.gateway_request() |
eai_gateway_tokens_total | compteur | type | metrics.gateway_tokens() |
eai_gateway_cost_total | compteur | currency | metrics.gateway_cost() |
eai_policy_decisions_total | compteur | result (allow, deny) | metrics.policy_decision() |
eai_quota_rejections_total | compteur | aucun | metrics.quota_rejection() |
eai_runs_active | jauge | aucun | metrics.set_runs_active(), run_started(), run_finished() |
eai_heartbeat_stale_runs | jauge | aucun | metrics.set_heartbeat_stale_runs() |
Le label route est le gabarit de la route (/v1/tenants/{tenant_id}), jamais le chemin brut : la cardinalité reste bornée et aucun identifiant n'apparaît. Les requêtes sans route sont comptées avec route="unmatched". Les séries *_created sont désactivées.
/metrics n'est pas authentifié : il n'est pas publié par APISIX et la NetworkPolicy du control plane n'accepte que la passerelle, le portail et le namespace d'observabilité.
Alignement :
- ServiceMonitor
templates/control-plane/monitoring.yaml: sélection surapp.kubernetes.io/component: control-plane, porthttp, chemin/metrics. - Règles :
deploy/observability/prometheus/rules/eai-slo-cost.yamlet sa copiedeploy/observability/k8s/prometheusrule-eai.yaml(identiques, vérifié par test), plus les alertes du chart. Règles d'enregistrementeai:http_requests:error_ratio_{5m,1h,6h},eai:http_request_duration_seconds:p95_5m,eai:gateway_cost:rate1h,eai:gateway_tokens:rate5m. - La plateforme ne livre pas de tableaux de bord (ADR-0169) : les écrans du portail et le Navin Manager lisent l'API et ClickHouse ; un Grafana du client qui lit ce Prometheus n'utilise que les métriques
eai_*ci-dessus, les règles d'enregistrement, et les exportateurs externesvllm:*(vLLM),DCGM_FI_*(DCGM),litellm_*(LiteLLM),kube_*,container_*,node_*,up. Les métriques par tenant ont été retirées : le control plane n'expose pas de labeltenant(cardinalité) ; le détail par tenant reste dans l'API FinOps et ClickHouse.
OPA
Le control plane (eai/policy.py) envoie pour chaque politique engine: opa active du tenant :
POST {EAI_OPA_URL}/v1/data/eai/<paquet>/decision
{"input": {"subject": {...}, "action": "...", "resource": {...}, "context": {...}}}
input est exactement la requête du Policy Contract (contracts/v1/policy-contract.schema.json) ; resource.tenant_id est toujours le tenant de l'appelant. Le paquet est lu dans l'en-tête package eai.<paquet> du champ rego de la politique. Chaque paquet de deploy/policies/opa renvoie :
{"allow": bool, "reasons": ["code", ...], "obligations": [{"type": "dlp" | "approval" | "log_prompts", "params": {...}}]}
| Paquet | Action | Attributs lus |
|---|---|---|
eai.model_invocation | model.* | resource.attributes.provider, regions, data_classification_max, status, provider_retention_days, local ; context.data_classification, region, cost_estimate.amount ; subject.clearance |
eai.tool_invocation | tool.* | resource.attributes.risk_level, side_effects, required_scope, network_egress ; context.attributes.allowed_tools, sandbox_runtime_class ; subject.scopes (globs) |
eai.gateway_access | mcp.call, knowledge.query, memory.write, memory.search, realtime.session.create, connector.sync, connector.test, channel.inbound, multimodal.submit, agent.a2a.send | context.data_classification ; data.eai.config.kill_switch.gateways ; gateway_access.human_only_actions (actions refusées aux agents) ; appelé par policy.gate avant tout appel sortant |
eai.data_access | knowledge.*, data.* | resource.tenant_id, resource.attributes.data_classification, acl.users, acl.groups ; subject.groups, subject.clearance |
eai.agent_autonomy | agent.* | resource.attributes.data_classification_max, regions, risk_level ; context.attributes.autonomy_level, depth, steps, action_name, action_kind, amount |
La configuration est dans deploy/policies/opa/data.json (data.eai.config) : kill switch (global, tenants, fournisseurs, modèles, outils, agents), rangs de classification (public à secret, y compris restricted), seuils par paquet, et surcharges par tenant dans tenants.<uuid>.<paquet>. L'obligation approval est celle que lit domains/tools/service.py.
Activer un paquet pour un tenant : créer une politique engine: opa dont le rego déclare le paquet, par exemple package eai.model_invocation suivi de import rego.v1. Le control plane téléverse ce module (PUT /v1/policies/<tenant>-<nom>), qui complète le paquet déjà chargé. Si OPA est injoignable, la décision est un refus (policy engine unavailable: <nom>).
OpenFGA
deploy/policies/openfga/model.fga déclare, pour chaque type de ressource (tenant, project, agent, model, tool, knowledge_base, collection, document, dataset, prompt, connector, secret), les relations écrites et vérifiées par /v1/authz et le repli intégré : owner, editor, viewer, can_use, member, avec owner qui implique editor qui implique viewer, et owner qui implique can_use (openfga.IMPLIED). Les relations plus riches (admin, can_invoke, can_read, héritage from tenant / from project) restent disponibles.
Isolation : OpenFGA n'a qu'un store, donc le control plane préfixe les objets et les sujets non utilisateurs par l'UUID du tenant (agent:<tenant>/invoice, group:<tenant>/finance#member). Les utilisateurs restent globaux (user:<sub OIDC>), user:* est un joker. Les écritures utilisent on_duplicate: ignore et les suppressions on_missing: ignore, comme la table intégrée.
Amorçage :
fga store create --name enterprise-ai --model deploy/policies/openfga/model.fga --api-url http://openfga:8080 --api-token "$OPENFGA_PRESHARED_KEY"
bao kv patch kv/eai/prod/control-plane EAI_OPENFGA_STORE_ID=<store> EAI_OPENFGA_MODEL_ID=<modele>
EAI_OPENFGA_API_TOKEN (facultatif) est envoyé en Authorization: Bearer (clé prépartagée OpenFGA). Sans EAI_OPENFGA_STORE_ID, le control plane utilise la table eai.authz_tuples. Les tests du modèle : python3 tools/policies/fga_build.py --check.
Keycloak
deploy/keycloak/realm-enterprise-ai.json produit les jetons que lit eai/auth.py :
- Émetteur
https://<keycloak>/realms/enterprise-ai(EAI_OIDC_ISSUER), audiencecontrol-plane(EAI_OIDC_AUDIENCE) ajoutée par le mapperaudience-control-plane. - Scope
basic: claimsub(obligatoire). Scopeeai-tenant:tenant(slug du tenant, attribut utilisateur),clearance,rolesetrealm_access.roles. Scopeprofile:preferred_username, affiché comme nom du principal. - Rôles realm :
platform_admin,tenant_admin,security_officer,finops,developer,auditor,agent,viewer; rôle par défaut compositedefault-roles-enterprise-aiqui donneviewer. - Profil utilisateur déclaratif :
tenantetclearancene sont modifiables que par un administrateur ;clearanceest limité aux classifications du backend. - Clients :
portal(public, PKCE S256, redirectionshttp://localhost:3000/auth/callbackethttps://<portal-host>/auth/callback, à remplacer par l'hôte de production),control-plane(bearer uniquement, porteur de l'audience),cli(public, device flow). - Le compte
platform-adminn'a pas d'attributtenant: un jeton avec un tenant inconnu est refusé (unknown_tenant).
Variables d'environnement
Toute variable EAI_* posée par le chart (ConfigMap, Secret, env) ou par Compose (environment, .env.example) est un paramètre réel (config.py ou adaptateur d'intégration). Corrections apportées :
| Avant | Après |
|---|---|
EAI_OPENBAO_ADDR | EAI_OPENBAO_URL |
EAI_TEMPORAL_ADDRESS (gRPC 7233) | EAI_TEMPORAL_URL (API HTTP 7243) |
EAI_AUDIT_BUCKET | EAI_S3_BUCKET |
EAI_ENV, EAI_LOG_LEVEL, EAI_REDIS_URL, EAI_REGION, EAI_SPIFFE_TRUST_DOMAIN, EAI_POD_NAME | supprimées (non lues) |
EAI_WORKER_STREAM, EAI_WORKER_CONSUMER (workers GPU) | arguments --stream, --consumer |
EAI_REGISTRY, EAI_VERSION (Compose) | IMAGE_REGISTRY, IMAGE_TAG |
Ajouts : EAI_OTEL_ENABLED, EAI_CORS_ORIGINS, EAI_OPENBAO_KV_MOUNT=kv, EAI_MASTER_KEY, EAI_OPENBAO_TOKEN, EAI_OPENFGA_STORE_ID, EAI_OPENFGA_MODEL_ID, EAI_OPENFGA_API_TOKEN, EAI_QDRANT_API_KEY, EAI_CLICKHOUSE_USER, EAI_CLICKHOUSE_PASSWORD. EAI_CORS_ORIGINS accepte une liste séparée par des virgules (https://<a-host>,https://<b-host>) ou une liste JSON ('["https://<portal-host>"]'), comme toutes les listes EAI_*.
Les en-têtes exposés par CORS sont X-Request-Id, traceparent, X-EAI-Decision-Id, X-EAI-Cost-Estimated, X-EAI-Cost-Measured et Retry-After.
Politique OpenBao deploy/openbao/policies/control-plane.hcl : chemins kv/data/tenants/* et clés transit tenant-<uuid>, ceux qu'utilise integrations/openbao.py.
Vérifications
/tmp/toolchains/opa test deploy/policies/opa -v
python3 tools/policies/fga_build.py --check
helm lint deploy/helm/enterprise-ai -f deploy/helm/enterprise-ai/values-prod.yaml
helm template eai deploy/helm/enterprise-ai -f deploy/helm/enterprise-ai/values-onprem-gpu.yaml
terraform -chdir=deploy/terraform/envs/eks-example init -backend=false && terraform -chdir=deploy/terraform/envs/eks-example validate
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests/test_platform_*.py -q
Les tests OPA et OpenFGA démarrent les vrais binaires (/tmp/toolchains/opa, /tmp/toolchains/openfga, CLI /tmp/toolchains/fga, ou variables EAI_TEST_OPA_BIN, EAI_TEST_OPENFGA_BIN, EAI_TEST_FGA_CLI) et sont ignorés si un binaire manque.