« Agent Ready » : onboarder une équipe sur une plateforme agentique, sans ticket et avec des droits bornés

Troisième article de la série tirée du talk « IA agentique : vers Azure et au-delà ! » (Genève, 29 octobre 2026). Code complet dans aloizeau/agentic-ai-azure-and-beyond-geneve-2026, dossiers
terraform/modules/agent-readyetterraform/teams.
Une équipe de développement qui veut son agent l'obtient en une exécution de pipeline approuvée : deux identités sans secret, un stockage, un namespace avec quota, une clé de passerelle et un dépôt GitHub prêt à déployer. C'était la démo B du talk. Automatiser la création est simple ; le plus dur est de donner au pipeline juste les droits nécessaires.
Deux mécanismes bornent ces droits : une condition ABAC sur la délégation de rôles Azure, et une ValidatingAdmissionPolicy CEL côté Kubernetes.
Deux équipes, deux pipelines
| Équipe IA (plateforme) | Équipe de développement | |
|---|---|---|
| Déclenche | agent-ready.yml (workflow_dispatch) |
un push sur main de son dépôt agent-<équipe> |
| Approbation | environnement GitHub ai-team, relecteur obligatoire |
aucune, elle ne touche que son namespace |
| Exécute | Terraform terraform/teams, un état par équipe |
« 1 · Image » (build vers ACR) puis « 2 · Déploiement » |
| Identité | id-<base>-ai-team, fédérée sur l'environnement ai-team |
id-<équipe>-pipeline, fédérée sur la branche main de son dépôt |
Les workflows de l'équipe de dev font une dizaine de lignes et appellent les workflows réutilisables de l'équipe IA (agent-image.yml, agent-deploy.yml). L'équipe IA change la façon de construire et de déployer à un seul endroit.
Ce montage tient grâce à un comportement d'OIDC : quand un dépôt appelle un workflow réutilisable d'un autre dépôt, le sujet du jeton reste le dépôt et la branche de l'appelant. La fédération de l'identité d'équipe peut donc viser repo:<owner>/agent-<équipe>:ref:refs/heads/main, même si le code exécuté vient du dépôt de la plateforme.
Ce qu'une équipe reçoit en un apply
Tout est dans terraform/teams/main.tf.
| Couche | Ressources | Usage |
|---|---|---|
| Azure | identité du pipeline, fédérée GitHub | pousser l'image et les documents, déployer |
| Azure | identité du serveur MCP, fédérée AKS (system:serviceaccount:<équipe>:docs-mcp) |
lire les documents |
| Azure | compte de stockage shared_access_key_enabled = false et conteneur documents |
aucune clé de compte à faire fuiter |
| Azure | 4 attributions de rôle : Blob Data Contributor, Blob Data Reader, AcrPush, AKS Cluster User | les droits des deux identités |
| Kubernetes | namespace agentic.io/team et agentic.io/agent-ready, ResourceQuota, LimitRange |
périmètre et limites de capacité |
| Kubernetes | clé virtuelle vk-<équipe> et sa copie gateway-key |
accès à /agents/v1 (article 1) |
| Kubernetes | RoleBinding agentic-team-deployer pour l'identité du pipeline |
déployer dans son namespace |
| GitHub | dépôt privé agent-<équipe> créé depuis le modèle, variables Actions, deux pipelines |
partir d'un squelette qui déploie déjà |
Chaque équipe a son état Terraform (teams/<équipe>.tfstate). Pour retirer une équipe, un terraform destroy sur cet état supprime tout ce qu'elle a reçu sans toucher aux autres.
Borner Azure : la délégation de rôles sous condition ABAC
Pour attribuer quatre rôles aux identités qu'il vient de créer, le pipeline a besoin de Role Based Access Control Administrator. Sans condition, ce rôle lui permettrait de s'attribuer Owner. Azure permet de contraindre la délégation par une condition ABAC, et le module agent-ready n'autorise que quatre rôles, vers des principaux de service uniquement.
Fichier : terraform/modules/agent-ready/main.tf
locals {
delegable = join(", ", [
local.role_ids.storage_blob_data_contributor, # pipeline d'équipe → ses documents
local.role_ids.storage_blob_data_reader, # serveur MCP d'équipe → ses documents
local.role_ids.acr_push, # pipeline d'équipe → registre
local.role_ids.aks_cluster_user, # pipeline d'équipe → kubeconfig Entra ID
])
delegation_condition = <<-EOT
(
(
!(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
)
OR
(
@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegable}}
AND
@Request[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
)
)
AND
(
(
!(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
)
OR
(
@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegable}}
AND
@Resource[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
)
)
EOT
}
resource "azurerm_role_assignment" "ai_team_delegation" {
for_each = local.delegation_scopes # rg-<base>-equipes, le registre, le cluster
scope = each.value
role_definition_id = local.role_def.rbac_administrator
principal_id = azurerm_user_assigned_identity.ai_team[0].principal_id
principal_type = "ServicePrincipal"
condition_version = "2.0"
condition = local.delegation_condition
}
La condition a deux blocs symétriques. Le premier n'autorise l'écriture d'une attribution que si le rôle demandé (@Request) fait partie des quatre GUID et si le bénéficiaire est un principal de service ; le pipeline ne peut donc donner aucun droit à une personne ni à un groupe. Le second n'autorise la suppression que sur une attribution existante (@Resource) du même type. Sans lui, le pipeline pourrait supprimer l'attribution Owner d'un administrateur.
La portée compte autant que la condition. Le rôle n'est posé que sur le groupe de ressources des équipes, le registre et le cluster, et le pipeline n'est Contributeur que sur rg-<base>-equipes.
Pour vérifier, ajoutez ces commandes à une exécution du workflow agent-ready.yml, qui tourne sous l'identité de l'équipe IA, et tentez une attribution interdite. Elle doit échouer avec une erreur d'autorisation liée à la condition :
$rg = az group show -n rg-<base>-equipes --query id -o tsv
$sp = az identity show -g rg-<base>-equipes -n id-equipe-docs-pipeline --query principalId -o tsv
# Autorisé : un des quatre rôles, vers un principal de service
az role assignment create --assignee-object-id $sp --assignee-principal-type ServicePrincipal `
--role "Storage Blob Data Reader" --scope $rg
# Refusé : Contributor ne fait pas partie des rôles délégables
az role assignment create --assignee-object-id $sp --assignee-principal-type ServicePrincipal `
--role "Contributor" --scope $rg
Borner Kubernetes : RBAC pour les verbes, CEL pour le périmètre
Dans le cluster, le pipeline crée des namespaces, des quotas, des Secrets et des RoleBindings. RBAC sait autoriser la création de Secrets, mais pas la limiter aux namespaces d'équipe ou aux objets qui portent un label donné. La ClusterRole donne donc seulement les verbes :
Fichier : terraform/modules/platform/teams.tf
resource "kubernetes_cluster_role" "team_provisioner" {
metadata { name = "agentic-team-provisioner" }
rule {
api_groups = [""]
resources = ["namespaces", "resourcequotas", "limitranges", "secrets"]
verbs = ["get", "create", "update", "patch", "delete"]
}
rule {
api_groups = ["rbac.authorization.k8s.io"]
resources = ["rolebindings"]
verbs = ["get", "create", "update", "patch", "delete"]
}
rule {
# Sans bind, Kubernetes refuse un RoleBinding vers des droits
# que le pipeline ne détient pas lui-même (contrôle d'escalade).
api_groups = ["rbac.authorization.k8s.io"]
resources = ["clusterroles"]
verbs = ["bind"]
resource_names = ["agentic-team-deployer"]
}
}
Le verbe bind, limité à une seule ClusterRole, est peu connu. Il permet au pipeline de lier agentic-team-deployer sans en détenir lui-même les droits, et l'empêche de lier cluster-admin.
Le périmètre vient d'une ValidatingAdmissionPolicy (Kubernetes 1.30 et plus). Elle ne s'applique qu'à l'identité du pipeline et vérifie l'objet avant et après chaque modification :
Fichier : terraform/modules/platform/manifests/agent-ready-admission.yaml.tftpl
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: agent-ready-perimetre
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["namespaces", "secrets", "resourcequotas", "limitranges"]
- apiGroups: ["rbac.authorization.k8s.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["rolebindings"]
matchConditions:
- name: pipeline-agent-ready
expression: request.userInfo.username == '${principal_id}'
variables:
- name: objets # le nouvel objet ET l'existant
expression: >-
request.operation == 'CREATE' ? [object] :
(request.operation == 'DELETE' ? [oldObject] : [object, oldObject])
- name: equipe # le namespace cible est-il un namespace Agent Ready ?
expression: >-
request.resource.resource != 'namespaces' &&
has(namespaceObject.metadata.labels) &&
'agentic.io/agent-ready' in namespaceObject.metadata.labels &&
namespaceObject.metadata.labels['agentic.io/agent-ready'] == 'true'
validations:
- expression: >-
request.resource.resource != 'namespaces' ||
variables.objets.all(o, has(o.metadata.labels) &&
'agentic.io/agent-ready' in o.metadata.labels && o.metadata.labels['agentic.io/agent-ready'] == 'true' &&
'agentic.io/team' in o.metadata.labels && o.metadata.labels['agentic.io/team'] == o.metadata.name)
message: "namespace hors périmètre Agent Ready"
- expression: >-
request.resource.resource != 'secrets' || variables.equipe ||
(request.namespace == 'agentgateway-system' &&
variables.objets.all(o, o.metadata.name.startsWith('vk-') && has(o.metadata.labels) &&
'agentic.io/virtual-key' in o.metadata.labels && o.metadata.labels['agentic.io/virtual-key'] == 'true' &&
'agentic.io/agent-ready' in o.metadata.labels && o.metadata.labels['agentic.io/agent-ready'] == 'true'))
message: "Secret hors périmètre : namespace Agent Ready, ou clé virtuelle vk-<équipe> créée par Agent Ready"
- expression: >-
request.resource.resource in ['namespaces', 'secrets'] || variables.equipe
message: "quota, limites et RoleBinding : uniquement dans un namespace Agent Ready"
La vérification de oldObject empêche un détournement simple. Sans elle, le pipeline pourrait poser agentic.io/agent-ready=true sur le namespace kagent ou agentgateway-system, puis y créer ce qu'il veut. Comme l'objet existant doit déjà être dans le périmètre, ce réétiquetage échoue. La deuxième validation protège les clés de la plateforme : dans agentgateway-system, le pipeline n'atteint que les Secrets vk-* marqués par Agent Ready.
kubectl auth can-i ne teste que RBAC, pas l'admission. Pour tester le périmètre, faites un dry-run côté serveur en vous faisant passer pour l'identité du pipeline, avec le kubeconfig admin. Sur AKS avec Entra ID, Kubernetes connaît une identité managée par son object ID :
$oid = az identity show -g <rg-plateforme> -n id-<base>-ai-team --query principalId -o tsv
# Doit passer : un namespace d'équipe correctement marqué
@'
apiVersion: v1
kind: Namespace
metadata:
name: equipe-test
labels: { agentic.io/team: equipe-test, agentic.io/agent-ready: "true" }
'@ | kubectl apply --dry-run=server --as=$oid -f -
# Doit être refusé : « namespace hors périmètre Agent Ready »
kubectl label namespace kagent agentic.io/agent-ready=true --dry-run=server --as=$oid
# Doit être refusé : Secret de la plateforme
kubectl -n agentgateway-system create secret generic vk-platform --from-literal=x=y `
--dry-run=server --as=$oid
Dans les pipelines, affichez kubectl auth whoami. C'est le moyen le plus rapide de confirmer que Kubernetes voit bien l'object ID attendu par la matchCondition et par le RoleBinding.
Ce que l'équipe déploie
Dans son namespace, l'équipe reçoit la ClusterRole agentic-team-deployer : Deployments, Services, ConfigMaps, NetworkPolicies et les ressources kagent agents, remotemcpservers et modelconfigs. Elle n'a accès ni aux Secrets, ni au RBAC, ni aux namespaces.
Pour l'équipe, ajouter un agent revient à ouvrir une pull request. Le ModelConfig pointe vers la passerelle, avec la clé créée par Agent Ready :
Fichiers : terraform/teams/template/agent/modelconfig.yaml et agent.yaml
apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
name: passerelle-llm
spec:
provider: OpenAI
model: llm # ignoré : la passerelle choisit le déploiement
apiKeySecret: gateway-key
apiKeySecretKey: key
openAI:
baseUrl: http://agentgateway-proxy.agentgateway-system.svc.cluster.local/agents/v1
---
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: assistant-docs
spec:
description: Répond aux questions de l'équipe à partir de ses documents
type: Declarative
declarative:
modelConfig: passerelle-llm
systemMessage: |
Réponds en français, uniquement à partir des documents de l'équipe :
cherche d'abord avec « rechercher », lis avec « lire_document »,
et cite toujours le document source.
tools:
- type: McpServer
mcpServer:
apiGroup: kagent.dev
kind: RemoteMCPServer
name: docs-mcp
toolNames: [lister_documents, lire_document, rechercher]
Le serveur MCP de l'équipe lit son stockage par Workload Identity, sans clé :
Fichier : terraform/teams/template/mcp/server.py
container = ContainerClient(
account_url=f"https://{ACCOUNT}.blob.core.windows.net",
container_name=CONTAINER,
credential=DefaultAzureCredential(exclude_interactive_browser_credential=True),
)
mcp = FastMCP("docs-equipe", host="0.0.0.0", port=8080, stateless_http=True)
@mcp.tool()
def lire_document(nom: str) -> str:
"""Renvoie le contenu d'un document (nom exact, tel que listé)."""
docs = _documents() # cache de 60 s sur le conteneur
if nom not in docs:
return f"Document introuvable : {nom}. Documents : {', '.join(sorted(docs))}"
return docs[nom][:MAX_CHARS]
Pour publier ce serveur sur la passerelle, l'équipe n'ouvre aucun ticket. Il suffit d'un label, d'un appProtocol et de deux annotations sur le Service :
Fichier : terraform/teams/template/deploy/docs-mcp.yaml
apiVersion: v1
kind: Service
metadata:
name: docs-mcp
labels:
agentic.io/publier-mcp: "true"
annotations:
agentgateway.dev/mcp-path: /mcp
agentgateway.dev/mcp-target-name: docs-equipe-docs
spec:
selector: { app: docs-mcp }
ports:
- name: mcp
port: 8080
targetPort: mcp
appProtocol: agentgateway.dev/mcp
Côté plateforme, un seul backend découvre ces Services dans tous les namespaces d'équipe :
Fichier : gateway/deploy/backend-mcp.yaml
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: mcp-equipes
spec:
mcp:
prefixMode: Always # docs-equipe-docs_rechercher : on voit quelle équipe sert l'outil
failureMode: FailOpen # le serveur en panne d'UNE équipe ne coupe pas les autres
targets:
- name: equipes
selector:
namespaces:
matchExpressions:
- { key: agentic.io/team, operator: Exists }
services:
matchLabels:
agentic.io/publier-mcp: "true"
Dans LibreChat, les utilisateurs voient sur /mcp-equipes les outils de toutes les équipes, préfixés par le nom de l'équipe, avec leur jeton Entra ID. Le label agentic.io/team posé par Agent Ready sert à la fois au périmètre d'admission et à cette découverte.
Le modèle de dépôt rendu par Terraform
Le provider GitHub crée le dépôt agent-<équipe> depuis le dossier terraform/teams/template. Les variables y sont des marqueurs __EQUIPE__, __STORAGE__ ou __CLIENT_ID__ plutôt que ${}, pour ne pas entrer en collision avec ${{ }} des workflows ni avec ${VAR} du shell.
Deux règles de cycle de vie évitent les mauvaises surprises. Avec lifecycle { ignore_changes = [content] }, les fichiers appartiennent à l'équipe une fois créés, et relancer Agent Ready n'écrase ni son agent ni ses documents. Les workflows sont écrits en dernier (depends_on sur le contenu et les variables), pour qu'aucun run ne parte pendant que le dépôt se remplit.
Les limites que j'assume
Le pipeline lit les Secrets. Terraform relit ce qu'il gère, d'où le verbe get, et l'admission ne filtre pas les lectures ; le pipeline voit donc les Secrets des namespaces d'équipe.
L'état Terraform contient les clés. Un état par équipe limite l'exposition, mais l'accès au stockage d'état doit rester réservé à l'équipe IA.
AcrPush est global au registre, si bien qu'une équipe peut pousser une image sous le nom d'une autre. Les permissions par dépôt d'ACR, ou un registre par équipe, ferment ce point.
Un rôle tout juste attribué peut mettre quelques minutes à s'appliquer ; les pipelines réessaient au lieu d'échouer. Enfin, l'authentification Entra ID d'AKS ne se désactive plus une fois activée. Terraform et le présentateur gardent le kubeconfig admin, et les équipes passent par kubelogin.
Le code
terraform/modules/agent-ready/: identité de l'équipe IA et délégation ABACterraform/modules/platform/teams.tfetagent-ready-admission.yaml.tftpl: ClusterRoles et politique d'admissionterraform/teams/: la pile par équipe, son modèle de dépôt et ses tests.github/workflows/:agent-ready.yml,agent-image.yml,agent-deploy.ymldemos/B-agent-equipe/: la démo de bout en bout
Le prochain article présente un agent d'exploitation qui s'appuie sur les MCP d'entreprise, avec approbation humaine.
About the author
Ayant eu de multiples expériences dans le monde du conseil, j'ai pu acquérir une expertise dans la conception et la construction de services de collaboration d'entreprise. Je suis passionné par les technologies Microsoft et surtout Azure. Aujourd'hui je possède une solide expérience en méthodologie de développement et j'ai mené des équipes de développement technique au succès. Je possède également une solide connaissance de l'infrastructure qui fait de moi une ressource efficace pour mettre en œuvre la transformation numérique vers le cloud Microsoft. Je suis un professionnel efficace et honnête qui aime relever les challenges. Aimant partager mes connaissances; je suis à l'aise en tant que Technical Leader et en tant que membre d'une équipe. Mes compétences techniques sont les suivantes: Azure, DevOps, Architecture Applicative, Développement de solution Cloud Native, écosystème Microsoft... et bien d'autres. Pour voir mes certifications Microsoft : https://www.youracclaim.com/users/antoine-loizeau
Comments
Join the discussionNo comments yet. Be the first to comment.
IA agentique : vers Azure et au-delà !
Série d'article tirée du talk « IA agentique : vers Azure et au-delà ! » (Chalet Azure Romandie, Genève, 29 octobre 2026). Tout le code est dans le dépôt aloizeau/agentic-ai-azure-and-beyond-geneve-2026.





