Aller au contenu principal

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é par worker.enabled: true (valeur par défaut). Une seule réplique, stratégie Recreate, 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 que control-plane (ancre YAML x-control-plane-env), sans port publié.
  • Supervision : l'alerte EaiStaleHeartbeats se déclenche quand eai_heartbeat_stale_runs reste 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étriqueTypeLabelsSource
eai_http_requests_totalcompteurmethod, route, statusmiddleware HTTP
eai_http_request_duration_secondshistogrammemethod, routemiddleware HTTP
eai_gateway_requests_totalcompteurprovider, model, outcomemetrics.gateway_request()
eai_gateway_tokens_totalcompteurtypemetrics.gateway_tokens()
eai_gateway_cost_totalcompteurcurrencymetrics.gateway_cost()
eai_policy_decisions_totalcompteurresult (allow, deny)metrics.policy_decision()
eai_quota_rejections_totalcompteuraucunmetrics.quota_rejection()
eai_runs_activejaugeaucunmetrics.set_runs_active(), run_started(), run_finished()
eai_heartbeat_stale_runsjaugeaucunmetrics.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 sur app.kubernetes.io/component: control-plane, port http, chemin /metrics.
  • Règles : deploy/observability/prometheus/rules/eai-slo-cost.yaml et sa copie deploy/observability/k8s/prometheusrule-eai.yaml (identiques, vérifié par test), plus les alertes du chart. Règles d'enregistrement eai: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 externes vllm:* (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 label tenant (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": {...}}]}
PaquetActionAttributs lus
eai.model_invocationmodel.*resource.attributes.provider, regions, data_classification_max, status, provider_retention_days, local ; context.data_classification, region, cost_estimate.amount ; subject.clearance
eai.tool_invocationtool.*resource.attributes.risk_level, side_effects, required_scope, network_egress ; context.attributes.allowed_tools, sandbox_runtime_class ; subject.scopes (globs)
eai.gateway_accessmcp.call, knowledge.query, memory.write, memory.search, realtime.session.create, connector.sync, connector.test, channel.inbound, multimodal.submit, agent.a2a.sendcontext.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_accessknowledge.*, data.*resource.tenant_id, resource.attributes.data_classification, acl.users, acl.groups ; subject.groups, subject.clearance
eai.agent_autonomyagent.*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), audience control-plane (EAI_OIDC_AUDIENCE) ajoutée par le mapper audience-control-plane.
  • Scope basic : claim sub (obligatoire). Scope eai-tenant : tenant (slug du tenant, attribut utilisateur), clearance, roles et realm_access.roles. Scope profile : 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 composite default-roles-enterprise-ai qui donne viewer.
  • Profil utilisateur déclaratif : tenant et clearance ne sont modifiables que par un administrateur ; clearance est limité aux classifications du backend.
  • Clients : portal (public, PKCE S256, redirections http://localhost:3000/auth/callback et https://<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-admin n'a pas d'attribut tenant : 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 :

AvantAprès
EAI_OPENBAO_ADDREAI_OPENBAO_URL
EAI_TEMPORAL_ADDRESS (gRPC 7233)EAI_TEMPORAL_URL (API HTTP 7243)
EAI_AUDIT_BUCKETEAI_S3_BUCKET
EAI_ENV, EAI_LOG_LEVEL, EAI_REDIS_URL, EAI_REGION, EAI_SPIFFE_TRUST_DOMAIN, EAI_POD_NAMEsupprimé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.