Agents on Call, partie 2. Les fondations : Terraform avant les tokens
Avant tout token Bedrock : le compte ops-tooling, les deux rôles IAM des spokes, l'accès aux modèles et le choix du profil d'inférence, tout en Terraform.

Avant que l'un des quatre agents de la Partie 1 puisse consulter un log, chiffrer le coût d'une charge de travail ou proposer un correctif, cette plateforme a besoin d'une frontière de compte et de deux rôles IAM qui font de la « lecture seule par défaut » une propriété imposée par AWS, pas une promesse inscrite dans un system prompt. Cette frontière, la demande d'accès aux modèles qui doit avoir lieu des jours avant que quiconque ne prévoie une démo, et l'arbitrage entre inférence on-demand, provisionnée et profils d'inférence cross-region, voilà ce qui se construit dans cette partie, entièrement en Terraform, avant qu'une seule ligne de code d'agent n'existe.
La Partie 1 a planté le décor : une entreprise SaaS de taille moyenne avec 30 comptes AWS et 40 alertes par semaine, qui choisit de construire une plateforme ops multi-agents sur Bedrock AgentCore et Strands plutôt que d'en acheter une, avec la lecture seule imposée par IAM comme contrainte de conception non négociable. Cette partie est celle où cette contrainte cesse d'être un diagramme et devient du HCL. Le code compagnon vit sur github.com/flightlesstux/agents-on-call, dans terraform/00-foundation/, et chaque extrait ci-dessous est tiré de ce répertoire, pas simplifié pour l'article.
Pourquoi un compte ops-tooling dédié
L'organisation des comptes de la Partie 1 place toute la plateforme d'agents dans un seul compte, séparé des quelque 30 comptes de workload sur lesquels elle opère :
| Compte | Rôle |
|---|---|
| management | Racine de l'org, SCP, aucun workload |
| security | Administration déléguée du CloudTrail de l'org, de GuardDuty et de Security Hub |
| ops-tooling | Toute la plateforme d'agents : Runtime, Gateway, Memory, la state machine Step Functions d'approbation |
| workload × ~30 | Spokes. Expose exactement deux rôles à ops-tooling |
Un compte unique et dédié, plutôt que de déployer l'infrastructure d'agents dans chaque compte de workload, apporte trois choses qu'un compte partagé ne peut pas offrir. D'abord, le rayon d'impact : une Lambda mal configurée ou un identifiant divulgué dans ops-tooling ne peut toucher directement rien dans un spoke, parce qu'atteindre un spoke exige toujours un sts:AssumeRole explicite à travers une frontière de compte, pas une permission implicite au sein d'un même compte. Ensuite, un seul endroit à auditer : le CloudTrail du compte security voit chaque appel cross-account d'assume-role depuis une source unique, au lieu de corréler l'activité des agents éparpillée sur 30 traces. Enfin, un rayon d'impact indépendant pour la plateforme elle-même : si ops-tooling subit un incident, tout le reste se dégrade au statu quo, exactement la propriété additive, jamais sur le chemin critique, que la Partie 1 a posée comme exigence.
Les deux rôles que chaque spoke expose
Chaque compte de workload expose exactement deux rôles à ops-tooling, et rien d'autre. Les deux vivent dans un module Terraform réutilisable, modules/spoke-roles/, destiné à être appliqué une fois par compte spoke : un alias de provider par compte fonctionne pour une poignée de spokes, et à environ 30 comptes, cela tourne plutôt depuis un pipeline par compte, par exemple un workspace Terraform Cloud par compte ou une personnalisation de Landing Zone Account Factory. Le module racine de ce dépôt l'instancie une fois contre un provider unique afin que l'exemple reste exécutable avec terraform validate.
ops-readonly : construit à partir de politiques managées, plafonné par un refus explicite
Les outils de lecture (cloudwatch-read, logs-read, cost-read) endossent tous ce rôle unique. Sa trust policy nomme précisément les ARN des rôles d'exécution Lambda autorisés à l'endosser, plus un external ID partagé comme second facteur contre le problème du confused deputy :
data "aws_iam_policy_document" "ops_readonly_trust" {
statement {
sid = "AssumeFromOpsToolingReadTools"
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = var.ops_readonly_assumer_role_arns
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}
resource "aws_iam_role" "ops_readonly" {
name = "ops-readonly"
assume_role_policy = data.aws_iam_policy_document.ops_readonly_trust.json
max_session_duration = 3600
}Les permissions viennent de deux politiques managées AWS : CloudWatchReadOnlyAccess couvre à la fois les métriques CloudWatch et les lectures CloudWatch Logs en une seule attache, ce qui explique pourquoi cloudwatch-read et logs-read peuvent partager un seul rôle IAM au lieu d'en avoir besoin de deux, et AWSBillingReadOnlyAccess couvre les appels Cost Explorer dont cost-read a besoin. Par-dessus les deux, une déclaration de refus explicite bloque une liste fixe d'actions mutantes (iam:*, ssm:StartAutomationExecution, ec2:TerminateInstances, s3:PutObject*, et une dizaine d'autres) quelles que soient les autorisations accordées par les politiques managées attachées. Ce dernier point n'est pas décoratif. Les politiques managées AWS se voient ajouter de nouvelles actions au fil du temps, à mesure que les services gagnent des fonctionnalités, et un refus explicite est la seule chose qu'une évaluation IAM ne peut jamais outrepasser avec un allow, quelle que soit la politique, ou la future version d'une politique, qui l'a produit.
ops-mutate : un seul appelant, une seule liste blanche, aucune politique managée
La trust policy est encore plus restreinte : exactement un principal, la Lambda exécutrice post-approbation, qui ne s'exécute qu'après qu'un humain a cliqué sur approuver dans Slack.
data "aws_iam_policy_document" "ops_mutate_trust" {
statement {
sid = "AssumeFromPostApprovalExecutorOnly"
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [var.ops_mutate_assumer_role_arn]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}La politique de permissions est la moitié la plus intéressante. ssm:StartAutomationExecution supporte une restriction au niveau des ressources vers des ARN de documents SSM Automation spécifiques, si bien que la liste blanche de runbooks que ce rôle peut exécuter est imposée directement par IAM, pas par une logique applicative qui pourrait avoir un bug :
data "aws_iam_policy_document" "ops_mutate_permissions" {
statement {
sid = "StartOnlyAllowlistedRunbooks"
effect = "Allow"
actions = ["ssm:StartAutomationExecution"]
resources = var.allowed_ssm_automation_document_arns
}
statement {
sid = "InspectAndStopOwnExecutions"
effect = "Allow"
actions = [
"ssm:GetAutomationExecution",
"ssm:DescribeAutomationExecutions",
"ssm:StopAutomationExecution",
]
resources = ["*"]
}
}La seconde déclaration porte sur Resource = "*" pour une raison précise : GetAutomationExecution et les autres appels d'inspection opèrent sur un ID d'exécution qui n'existe pas tant que StartAutomationExecution n'en a pas déjà retourné un, donc il n'y a rien à quoi les restreindre à l'avance. La frontière significative sur ce rôle porte sur les documents qu'il peut démarrer, pas sur celles de ses propres exécutions déjà en cours qu'il peut consulter. Et contrairement à ops-readonly, ce rôle n'a aucune attache de politique managée AWS. Chaque permission qu'il détient est énumérée dans ce seul document de politique, si bien qu'un changement côté AWS sur une politique managée ne peut jamais élargir silencieusement ce qu'il peut faire.
L'accès aux modèles Bedrock est un problème qui se compte en jours de console
Il n'existe aucune ressource Terraform pour accorder l'accès à un modèle de fondation Bedrock, et ce manque mérite d'être signalé avant qu'il ne coûte une session de débogage. Activer un modèle pour un compte est une étape manuelle, soit via la page « Model access » de la console Bedrock, soit via l'appel API ponctuel équivalent, qui déclenche un flux d'acceptation d'EULA qu'AWS traite de façon asynchrone. Ce n'est ni instantané, ni une infrastructure idempotente qu'un plan et un apply peuvent exprimer, donc cela ne peut pas vivre dans le même dépôt que les rôles IAM ci-dessus.
L'effet pratique : demander l'accès à chaque modèle que cette plateforme appellera, dans chaque région vers laquelle un profil d'inférence cross-region peut router, avant le premier terraform apply, pas après. Une entitlement manquante dans une région d'un profil multi-région ne fait échouer que les invocations routées vers elle, ce qui fait ressembler l'échec à quelque chose d'intermittent, un agent capricieux, un throttle transitoire, au lieu de ce que c'est réellement : une région sur plusieurs qui attend encore un clic de console que personne n'a fait. Budgétez cela comme une tâche du premier jour, pas une tâche en cours de test, parce que le flux d'approbation lui-même échappe au contrôle de Terraform, et à celui de ce projet.
On-demand, provisionné ou cross-region : la règle de décision est « on-demand jusqu'à preuve mesurée »
Trois façons existent d'appeler un modèle Bedrock, et choisir entre elles avant d'avoir le moindre trafic à mesurer est une supposition déguisée en décision d'architecture.
| Mode | Comment c'est facturé | Ce que ça apporte |
|---|---|---|
| On-demand | Par token d'entrée/sortie, tarif standard publié | Aucun engagement, redescend à zéro, le seul choix par défaut raisonnable avant d'avoir une ligne de base de trafic |
| Provisioned Throughput | À l'heure, par Model Units ; le tarif dépend du modèle, du nombre de MU et de la durée d'engagement (sans engagement, 1 mois, 6 mois, les engagements plus longs coûtent moins cher à l'heure) | Débit garanti et dédié ; facturé pour la capacité, qu'elle soit utilisée ou non |
| Profil d'inférence cross-region | Même tarif par token que la région source, aucun surcoût de routage | Routage automatique entre les régions d'une géographie pour de la marge en cas de pic et de la fiabilité, on-demand uniquement |
Les profils d'inférence cross-region sont le choix facile : ils ne coûtent rien de plus que l'on-demand et ajoutent de la capacité en cas de pic plus un chemin de failover si une région throttle, donc cette plateforme les utilise partout dès le premier jour. Provisioned Throughput est le choix difficile, et la règle que ce projet applique est on-demand jusqu'à preuve mesurée : une Model Unit achète un plafond fixe de tokens d'entrée et de sortie par minute, facturé à l'heure que la plateforme envoie ou non la moindre requête pendant une heure donnée, donc s'y engager avant de connaître la vraie distribution du taux de requêtes à travers quatre agents revient à s'engager sur un chiffre que personne ne peut encore défendre. La Partie 7 détaille le calcul réel en tokens et le point où le tarif horaire du provisioned throughput bat le tarif par token de l'on-demand ; tant que ce calcul n'existe pas, deviner un niveau d'engagement ne fait que déplacer le risque de « le modèle est lent » vers « la facture est fausse ».
Ce que Terraform crée réellement ici, ce n'est pas le profil cross-region lui-même, cette partie est gérée par AWS et existe dès qu'une région le supporte, mais un profil d'inférence applicatif par agent qui enveloppe le profil défini par le système :
resource "aws_bedrock_inference_profile" "agent" {
for_each = var.agents
name = "${var.platform_name}-${each.key}"
description = "Application inference profile for the ${each.key} agent, tagged for per-agent cost allocation."
model_source {
copy_from = each.value.cross_region_profile_arn
}
tags = merge(var.tags, {
Agent = each.key
})
}Le tag Agent est tout l'intérêt d'envelopper le profil système au lieu de l'appeler directement : sans lui, la dépense Bedrock de chaque agent atterrit dans une seule ligne indifférenciée, et l'exigence d'allocation des coûts par agent posée par l'architecture de la Partie 1 n'a rien sur quoi s'appuyer. Avec lui, la configuration Budgets et Cost Anomaly Detection propre à la plateforme, censée surveiller les agents qui surveillent tout le reste, peut réellement attribuer la dépense à l'agent qui l'a générée.
Le premier terraform apply
Exécuter terraform -chdir=terraform/00-foundation init puis plan donne un petit plan en nombre de lignes, deux rôles IAM, deux politiques inline, deux attaches de politiques managées, et autant de profils d'inférence applicatifs qu'il y a d'agents, mais c'est le seul apply de toute cette série qui doit être correct avant que quoi que ce soit en aval puisse être digne de confiance. Le module racine committé instancie spoke_roles une fois contre le provider par défaut, si bien que cet exemple s'applique dans un seul compte pour rester exécutable avec terraform validate et plan. Un déploiement réel passe plutôt un alias de provider scopé au spoke à ce module ; sans cela, les deux rôles atterrissent côte à côte dans un seul compte et la frontière cross-account que cette partie est censée construire n'existe pas. Chaque partie suivante suppose qu'ops-readonly et ops-mutate existent déjà exactement sous cette forme : se tromper ici sur la liste des principaux de la trust policy, et l'appel AssumeRole d'une Lambda outil échoue en Partie 4 d'une façon qui ressemble à une mauvaise configuration de Gateway, pas à une faute de frappe de la Partie 2 vieille de trois semaines.
Un détail d'implémentation qui a coûté quelques minutes la première fois : aws_iam_role.max_session_duration a un plancher strict de 3600 secondes. L'instinct pour ops-mutate, vu à quel point son rayon d'impact est déjà étroitement restreint, est de demander la session la plus courte possible, mais IAM n'autorise aucun plafond au niveau du rôle en dessous d'une heure. La durée de vie réelle des identifiants que l'exécuteur post-approbation demande à l'exécution, via son propre appel STS, peut et doit rester bien plus courte que ce plafond ; le réglage du rôle ne borne que le maximum, il ne fixe pas la valeur par défaut.
Hygiène de l'état et du backend
Rien dans cette partie ne touche à un backend distant, et c'est volontaire, parce que le point soulevé dans cette section compte plus que le backend précis choisi : l'état doit vivre quelque part avec verrouillage et chiffrement avant qu'une deuxième personne, ou un deuxième run de CI, n'applique jamais contre le même compte, et il doit vivre en dehors de tout ce qu'un agent ou une Lambda outil peut atteindre. Un bucket S3 avec versioning et une table de verrouillage DynamoDB, ou Terraform Cloud, fonctionnent tous les deux ; ce qui compte, c'est qu'ops-readonly et ops-mutate n'aient aucune permission de lire ou d'écrire cet état, et que les humains qui exécutent terraform apply contre ce dépôt empruntent un chemin différent et audité de celui de la plateforme que le dépôt construit. Le drift de console est l'autre moitié de cette discipline : des changements en production faits en cliquant dans la console désynchronisent le fichier d'état de la réalité, d'une façon qui ressurgit plus tard comme un plan déroutant, pas comme une erreur immédiate, ce qui est exactement l'argument en faveur de traiter l'IaC comme le seul chemin vers la production, développé plus en détail dans IaC-First: Why We Never Touch the AWS Console in Production sur ercan.cloud.
Modes de défaillance à surveiller, propres à cette partie
Trois choses à connaître avant qu'elles n'apparaissent comme un symptôme déroutant deux parties plus tard. Une entitlement de modèle manquante dans une région d'un profil cross-region ne fait échouer que les requêtes routées vers cette région, ce qui se présente comme une instabilité intermittente, pas comme une erreur nette, jusqu'à ce que quelqu'un pense à vérifier la page d'accès de la console plutôt que le code. Une faute de frappe dans ops_readonly_assumer_role_arns ou ops_mutate_assumer_role_arn fait échouer AssumeRole avec un access-denied qui ne donne aucun indice sur quel côté, trust policy ou appelant, est fautif, donc gardez ces ARN de rôle Lambda comme des outputs Terraform depuis l'endroit où ils sont créés et référencez-les, ne retapez jamais un ARN à la main. Et une liste allowed_ssm_automation_document_arns vide est du Terraform valide, le rôle s'applique proprement, s'endosse proprement, puis ne peut plus jamais rien démarrer réellement ; un bloc de validation au moment du plan attrape ce cas précis, mais uniquement parce que quelqu'un a pensé à l'ajouter, pas parce que Terraform l'attrape tout seul.
À lire ensuite
- Partie 1, Le scénario : pourquoi une équipe ops recrute des agents, pour l'inventaire des tâches répétitives de l'astreinte, le choix de la stack, et l'organisation des comptes que cette partie construit en Terraform.
- Partie 3, Premier agent : le triage d'incidents en Strands, où le premier agent Strands se déploie sur AgentCore Runtime en utilisant le rôle
ops-readonlyconstruit ici. - IaC-First: Why We Never Touch the AWS Console in Production sur ercan.cloud, l'argument sur le drift d'état et la discipline de console sur lequel s'appuie la section backend de cette partie, vu du côté infrastructure générale plutôt que du côté plateforme d'agents.
Le module terraform/00-foundation/ complet, y compris les parties retirées des extraits ci-dessus, vit dans le dépôt compagnon à github.com/flightlesstux/agents-on-call. Pour le volet base de données et infrastructure de faire tourner ce genre de plateforme à grande échelle, les notes de terrain sont sur ercan.cloud, et le hub se trouve sur ercanermis.com.
Plus d'Ercan
Deux autres sites, même auteur, terrain différent.
Cloud, AWS, EKS, Terraform, plateforme.
Notes de terrain de systèmes de production. EKS, IAM, Terraform à l'échelle organisation, observabilité, optimisation des coûts.
Visiter ercan.cloud →Le hub. À propos, conseil, contact.
Hub personnel pour les deux pistes d'écriture. Qui je suis, comment fonctionne le conseil, comment me joindre.
Visiter ercanermis.com →