Shared infrastructure, isolated tenants: Pool model multi-tenancy with Amazon Bedrock AgentCore

TL;DR · AI 摘要
AWS 通过 Amazon Bedrock AgentCore 实现多租户 AI 应用的基础设施共享与租户隔离,适用于 SaaS、企业解决方案等场景。
核心要点
- 使用 Amazon Bedrock AgentCore 可实现租户间完全隔离,确保数据安全。
- 通过服务层级(Tier)划分,可实现差异化服务质量与成本控制。
- 知识库、记忆、模型访问和成本追踪是实现租户隔离的关键技术点。
结构提纲
按章节快速跳转。
- §引言
构建多租户 AI 应用面临新的架构挑战,如租户隔离、服务质量、成本追踪等。
通过 Amazon Bedrock AgentCore 实现基础设施共享与租户隔离,采用 Tier → Tenant → User 三层架构。
通过服务层级(如 Basic Tier 和 Premium Tier)实现差异化服务质量与成本控制。
通过医疗 AI 助手示例展示如何在实际场景中实现多租户架构。
- §最佳实践
介绍构建可扩展多租户 AI 架构的最佳实践与技术要点。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 多租户 AI 架构
- Amazon Bedrock AgentCore
- 基础设施共享
- 租户隔离
- 服务层级划分
- 架构层次
- Tier
- Tenant
- User
金句 / Highlights
值得收藏与分享的关键句。
通过服务层级(Tier)划分,可实现差异化服务质量与成本控制。
租户隔离通过知识库、记忆、模型访问和成本追踪实现。
Amazon Bedrock AgentCore 支持基础设施共享与租户隔离,适用于 SaaS 和企业解决方案。
共享基础设施,隔离租户:使用 Amazon Bedrock AgentCore 的池模型多租户 | 人工智能
共享基础设施,隔离租户:使用 Amazon Bedrock AgentCore 的池模型多租户
构建多租户 AI 应用程序带来了新的架构挑战。你需要在客户之间实现完全的租户隔离,提供具有不同功能的不同服务层级,实现细粒度的成本跟踪,并按租户进行可观测性。没有这些,你可能会面临暴露客户数据、无法为客户提供适当的服务质量或产生不可预见的成本等风险。
在本文中,你将学习如何使用 Amazon Bedrock AgentCore 实现生产就绪的多租户系统的模式。你将通过服务于多个诊所和医院的医疗 AI 代理看到这些模式的演示。虽然本文以医疗保健作为示例领域,但架构模式和实现技术广泛适用于各种多租户 AI 应用程序。无论你是在构建 SaaS 平台、服务于多个业务部门的企业解决方案,还是为不同客户组织提供的托管服务,你都可以使用这些架构模式来构建你的解决方案。
你将学到的内容
- 如何使用 AWS 原生功能在代理应用程序中实现完全的租户隔离。
- 使用最少自定义代码实现服务层级差异化的模式。
- 每个租户进行细粒度成本归属的技术。
- 可扩展多租户 AI 架构的最佳实践。
这篇博客文章是系列文章《使用 Amazon Bedrock AgentCore 构建多租户代理》的第二部分。第一部分探讨了构建多租户代理应用程序的设计考虑因素,以及使用 Amazon Bedrock AgentCore 解决 SaaS 架构挑战所需的框架。
示例代码的 GitHub 仓库:https://github.com/aws-samples/sample-agentcore-and-multitenancy-blog
解决方案概述
该解决方案演示了如何使用 Amazon Bedrock AgentCore 的原生功能,通过 AWS 管理服务实现完全的租户隔离。该架构实现了一个三级层次结构:层级 → 租户 → 用户,其中在知识库、记忆、模型访问和成本跟踪的每个层级中,通过文档强制实施隔离。分层策略是 SaaS 应用程序中常见的模式,租户根据其需求被分组到不同的服务层级中,例如基础和高级、使用模式或定价计划。每个层级定义了一组功能和对组内租户可用的服务质量。这种方法使 SaaS 提供商能够以差异化的体验服务于多样化的客户群,同时保持运营效率。
医疗 AI 助手示例
为了了解这一方法在实际中的运作方式,示例解决方案实现了两个服务层级,用于基于层级的差异化:
- 基础层级:专为小型诊所和实践设计,主要需要简单的文档搜索和检索。由于这些任务非常适合小型、成本效益高的模型,该层级使用 Mistral Ministral 3 8B Instruct,从而在保持低成本的同时仍然为简单查询提供准确的结果。
- 高级层级:专为需要复杂临床分析的医院和专科中心设计。该层级使用 OpenAI GPT OSS 120B,具备高级推理能力,以实现准确的工具选择,包括仅高级层级客户可使用的网络搜索工具。
在每个层级中,该解决方案使用池隔离模型,其中租户共享相同的底层基础设施和计算资源,而不是为每个租户提供专用的、隔离的资源。池模型最大化了资源利用率并简化了操作,而租户隔离则通过逻辑分离机制(如作用域标识符、访问策略和数据分区)来实现。将层级策略与池模型结合,使您能够在成本效率与提供差异化服务等级的灵活性之间取得平衡。
架构
让我们看看 AgentCore 的原始组件如何协同工作,以解决这些多租户挑战。以下图表展示了该解决方案的多租户架构,显示了经过身份验证的用户如何通过特定层级的代理将请求发送到隔离的文档存储中:
图 1:具有层级隔离的多租户架构(层级 → 租户 → 用户)。
该解决方案由以下关键组件组成:
- Amazon Cognito:管理用户身份验证,并在 JSON Web Token(JWT)声明中存储租户元数据(层级、clinic_id、role)。这些声明从请求负载中提取并传播为租户上下文,使每个下游组件能够将其操作作用域限制到正确的租户。
- Amazon API Gateway:路由请求,并通过使用计划实施基于层级的速率限制。
- AWS Lambda:提取租户上下文并调用相应的 Amazon Bedrock AgentCore 代理。
- AgentCore 组件:运行时(代理执行)、内存(对话状态)、身份(代理身份管理)、网关(工具服务器)和策略(代理操作边界)。
- Amazon Simple Storage Service(Amazon S3):在分层的存储桶中存储临床文档,使用分层前缀结构实现租户隔离。
- Amazon Bedrock 知识库:提供语义搜索和元数据过滤,以将查询作用域限制到请求租户的文档。
- Amazon Bedrock 项目:通过成本分配标签实现按层级的成本跟踪。
解决方案概述
本节描述了该解决方案的关键方面。您运行部署脚本以设置该解决方案的基础设施和应用程序。本节中的代码摘录仅用于描述架构的关键方面是如何通过解决方案的组件来实现的。无需运行此处显示的任何命令或执行任何代码片段。
Amazon Bedrock AgentCore 组件
该架构利用了六个核心的 Bedrock AgentCore 功能来实现多租户:
AgentCore 运行时:AgentCore 运行时为该解决方案中的代理提供计算能力,每个代理会话执行都在一个隔离的微虚拟机中,以实现租户级别的计算隔离。它为每个层级托管单独的代理实例,每个实例都配置了适合该层级的模型和功能。
# 代理配置
config = TIER_CONFIG.get(tier, TIER_CONFIG["basic"])
model_id = config["default_model"]
# 项目 ID 从 SSM 获取
project_id = get_ssm_parameter(config["project_ssm"])传递给 OpenAIModel(高级版)以针对推理端点
self.model = OpenAIModel( client_args={"base_url": mantle_base_url, "api_key": api_key, "project": project_id}, model_id=model_id, )
AgentCore 身份:AgentCore 身份通过统一的基于 JWT 的认证模型来保障多租户架构的安全性。Cognito ID 令牌在运行时和网关边界验证用户身份,而工具 Lambda 则为其下游数据访问生成自己的作用域凭证。
每个 AgentCore 运行时都配置了一个入站 JWT 授权器,在执行代理代码之前验证 Cognito ID 令牌。ID 令牌在自定义声明中携带租户元数据:
声明
示例值
用途
sub
a4589458-8011-…
唯一用户标识(Cognito UUID)
iss
https://cognito-idp.us-east-1.amazonaws.com/us-east-1_AbCdEfG
令牌颁发者,由 AgentCore 运行时验证
aud
7rfbikfsm51j…
网络客户端 ID,由运行时的 allowedAudience 验证
token_use
id
标识这是一个 ID 令牌(而非访问令牌)
exp
1745446200
过期时间戳(默认:从签发起 1 小时)
cognito:username
dr.foster@hospital-a.com
登录用户名,用作 memory 隔离的 user_id
custom:tier
premium
路由到正确的模型、知识库和网关
custom:clinic_id
hospital-a
这是租户 ID。在知识库、memory 和 Amazon DynamoDB 中强制数据隔离
custom:role
physician
基于角色的访问控制(未来可扩展性)
授权器在代理部署期间进行配置:
AUTHORIZER_CONFIG='{"customJWTAuthorizer":{"discoveryUrl":"'$COGNITO_DISCOVERY_URL'","allowedAudience":["'$COGNITO_WEB_CLIENT_ID'"]}}'
agentcore configure --entrypoint main.py \ --name healthcare_basic \ --authorizer-config "$AUTHORIZER_CONFIG" \ --request-header-allowlist "Authorization"
AgentCore 网关也配置了 JWT 授权,使用相同的 Cognito 发现 URL 和受众。当代理调用网关时,它会将用户的原始 JWT 作为 Bearer 令牌进行验证,并附带租户上下文头(X-Tier、X-Clinic-ID、X-S3-Prefix)。网关验证令牌后,通过 metadataConfiguration 将租户头传播到目标 Lambda。
目标 Lambda 从不直接接收或处理用户的 JWT。相反,它读取可信的租户头(可信是因为只有经过网关 CUSTOM_JWT 授权器验证的请求才能通过),并使用从这些头派生的会话标签假设 TVM(令牌售卖机)角色。TVM 角色的 ABAC 策略使用 dynamodb:LeadingKeys 条件限制 DynamoDB 访问,确保每个租户只能在 IAM 层查询其自己的诊所数据,而不仅仅是应用层过滤。
AgentCore Memory:会话历史不能在租户之间或租户内的多个用户之间泄露。解决方案在两个层面上强制执行 memory 隔离:应用层作用域和基于 IAM 的基于属性的访问控制(ABAC)。
在应用层,AgentCore Memory 使用带有复合 actor_id 的分层命名空间结构来按租户组织会话数据:
actor_id = f"{tier}-{clinic_id}-{user_id}"
示例:"basic-clinic-a-dr.smith@clinic-a.com"
命名空间将不同类型的 memory 分开:
clinic/{actor_id}/facts/{session_id} # SEMANTIC --- 临床事实 clinic/{actor_id}/preferences # PREFERENCES -- 用户偏好
为了在基础设施层面实现隔离,该解决方案使用了带有 ABAC 的 Token Vending Machine(TVM)模式。在运行时,代理会以 TVM 角色运行,使用 Tier、ClinicId 和 UserId 作为会话标签,获取临时凭证,这些凭证仅限于租户的命名空间:
sts = boto3.client("sts", region_name=region)
response = sts.assume_role( RoleArn=tvm_role_arn, RoleSessionName=f"mem-{tier}-{clinic_id}-{user_id}", DurationSeconds=900, Tags=[ {"Key": "Tier", "Value": tier}, {"Key": "ClinicId", "Value": clinic_id}, {"Key": "UserId", "Value": user_id}, ], TransitiveTagKeys=["Tier", "ClinicId", "UserId"], )
从临时凭证创建一个作用域限定的 boto3 会话
scoped_session = boto3.Session( aws_access_key_id=response["Credentials"]["AccessKeyId"], aws_secret_access_key=response["Credentials"]["SecretAccessKey"], aws_session_token=response["Credentials"]["SessionToken"], )
使用作用域限定的凭证构建 MemoryClient
memory_client = MemoryClient(region_name=region) memory_client.gmcp_client = scoped_session.client("bedrock-agentcore-control") memory_client.gmdp_client = scoped_session.client("bedrock-agentcore")
TVM 角色的信任策略确保只有代理执行角色可以担任该角色,并且所有三个会话标签都必须存在:
AssumeRolePolicyDocument: Statement:
- Effect: Allow
Principal: AWS: !GetAtt RuntimeAgentCoreRole.Arn Action:
- sts:AssumeRole
- sts:TagSession
Condition: StringLike: aws:RequestTag/Tier: "?*" aws:RequestTag/ClinicId: "?*" aws:RequestTag/UserId: "?*"
AgentCore 网关:AgentCore 网关通过使用 Model Context Protocol(MCP)将静态的 Lambda 函数转换为动态、上下文感知的代理工具。Model Context Protocol 是一种用于将 AI 代理连接到外部工具的开源标准。
AgentCore 网关消除了构建自定义工具编排逻辑的需要。如果没有这个功能,您将需要手动将 API 集成到代理的工作流中。这涉及编写自定义代码来解析 API 规范、处理身份验证、管理转换、实现错误处理,并传播租户上下文。
Lambda 函数通过网关暴露了两个工具:
- patient_context:从 PatientMetadata DynamoDB 表中检索患者人口统计信息和病史。
- clinic_config:从 ClinicConfig DynamoDB 表中获取诊所配置和提供者信息。
如前所述,租户身份在整个每个组件中都会传播。代理使用租户限定的标头(X-Tier、X-Clinic-ID、X-S3-Prefix)初始化其 MCP 网关客户端,因此通过网关的每个工具调用都会自动携带租户上下文,从而在网关层强制执行数据隔离,而无需每个工具的过滤逻辑。此链接提供了有关网关标头的更多信息。
将 Lambda 定义为 MCP 目标
lambda_target_config = { "mcp": { "lambda": { "lambdaArn": lambda_function_arn, "toolSchema": {"inlinePayload": api_specification} } } }
# 使用 AWS IAM 授权创建网关
# 代理的 Runtime 执行角色通过 SigV4 进行身份验证
gateway = gateway_client.create_gateway(
name="healthcare-basic-gw",
roleArn=execution_role_arn,
protocolType="MCP",
authorizerType="AWS_IAM",
description="Healthcare Clinical Document Processing Gateway",
)
# 添加带有租户头传播的 Lambda 目标
metadata_config = {
"allowedRequestHeaders": [
"X-Tier",
"X-Clinic-ID",
"X-S3-Prefix"
]
}
credential_config = [{"credentialProviderType": "GATEWAY_IAM_ROLE"}]
create_target_response = gateway_client.create_gateway_target(
gatewayIdentifier=gateway_id,
name=f"HealthcareLambda-{tier.title()}",
targetConfiguration=lambda_target_config,
credentialProviderConfigurations=credential_config,
metadataConfiguration=metadata_config,
)
网关支持三种身份验证机制:
- IAM 角色:用于 AWS 服务集成。
- 自定义 JWT:用于租户感知工具(我们正在使用的方式)。
- OAuth:用于第三方 API 集成。
AgentCore 策略:AgentCore 策略使用 Cedar 授权策略对网关工具实施特定层级的操作边界。解决方案创建了一个共享的策略引擎,并以 ENFORCE 模式附加到基本网关和高级网关。对于基本层级,Cedar 策略通过评估工具输入中的 request_hour 字段,将 patient_context 工具限制在工作时间(上午 8 点至下午 6 点)。代理必须首先调用 current_time 并传递当前小时,如果小时超出允许范围,策略引擎将拒绝该调用。对于高级层级,策略无条件允许 patient_context 工具,使医院可以全天候访问。两个层级都对 clinic_config 工具有明确的许可,因为它暴露的是非敏感的配置数据。这种方法将访问控制从应用程序代码中移出,并通过在网关层评估的声明式 Cedar 策略进行管理,因此层级区分在 Lambda 函数执行之前就已经强制实施。
# Cedar 策略:基本层级 --- 限制 patient_context 为工作时间
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"HealthcareLambda-Basic___patient_context",
resource == AgentCore::Gateway::"{gateway_arn}"
)
when {
context.input has request_hour &&
context.input.request_hour >= 8 &&
context.input.request_hour < 18
};
# Cedar 策略:高级层级 --- 全天候 patient_context 访问
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"HealthcareLambda-Premium___patient_context",
resource == AgentCore::Gateway::"{gateway_arn}"
)
when {
context.input has patient_id
}; AgentCore 可观测性:AgentCore 的可观测性集成使用 OpenTelemetry baggage 来在整个请求生命周期中传播租户元数据。OpenTelemetry baggage 是一个键值存储,允许您在跟踪上下文中传播附加数据。解决方案在 AgentCore Runtime 入口点将租户标识符设置为 baggage,因此每个下游跨度和日志条目都携带租户归属信息:
from opentelemetry import baggage, context
# 在 OTel baggage 中设置租户上下文(在 AgentCore Runtime 入口点)
ctx = baggage.set_baggage("tier", tier)
ctx = baggage.set_baggage("clinic_id", clinic_id, context=ctx)
ctx = baggage.set_baggage("actor_id", actor_id, context=ctx)
context.attach(ctx) 例如,您可以使用 Amazon CloudWatch Logs Insights 来跟踪每个诊所的请求数量
-- 从 API Gateway Lambda 代理日志中获取每个诊所的请求数量
fields @timestamp, @message
| filter @message like /Tier:/
| parse @message *"*Tier: *, Clinic ID: *, User ID: *, Role: **"* as tier, clinic_id, user_id, role
| stats count() as request_count by clinic_id, tier
| sort request_count desc结合 Bedrock Projects 实现按层级的费用归属,以及结构化使用日志实现按诊所的令牌跟踪,这使您能够获得模型使用、代理执行和内存操作的租户级可见性。
关键的多租户实现模式
本节描述了解决方案如何通过 Amazon Bedrock AgentCore 实现多租户的核心模式。
1. 通过每个层级的 S3 存储桶实现数据隔离
在医疗保健解决方案示例中,系统为每个服务层级创建一个独立的 S3 存储桶,并在每个存储桶内使用特定租户的前缀。每个层级的知识库都有自己的专用 S3 存储桶,从而在层级之间实现存储桶级别的隔离。在每个存储桶中,层级前缀组织租户数据,通过基于路径的访问控制和知识库元数据过滤实现隔离:
s3://healthcare-basic-kb-{suffix}/
├── basic-tier/
│ ├── clinic-a/
│ │ ├── appointment-notes/
│ │ ├── lab-results/
│ │ ├── patient-intake/
│ │ └── prescriptions/
│ ├── clinic-b/
│ ├── clinic-c/
│ └── clinic-d/
s3://healthcare-premium-kb-{suffix}/
├── premium-tier/
│ ├── hospital-a/
│ │ ├── surgical-notes/
│ │ ├── pathology-reports/
│ │ └── imaging-studies/
│ ├── hospital-b/
│ ├── clinic-e/
│ └── clinic-f/在每个存储桶中,S3 前缀由从 Cognito JWT 声明中提取的租户身份(custom:tier, custom:clinic_id)构建。该前缀以两种方式使用:作为每个 MCP Gateway 工具调用的 X-S3-Prefix 请求头传递,以实现网关级别的强制执行;文档检索工具则通过 Amazon Bedrock 知识库元数据过滤器在 clinic_id 上强制隔离:
response = client.retrieve(
knowledgeBaseId=kb_id,
retrievalQuery={"text": query},
retrievalConfiguration={
"vectorSearchConfiguration": {
"filter": {"equals": {"key": "clinic_id", "value": clinic_id}},
}
},
)2. 通过 Bedrock Projects 和结构化使用日志实现费用归属
费用归属在两个层级上进行:通过 Bedrock Projects 实现按层级的归属,通过结构化使用日志实现按诊所的归属。
通过 Bedrock Projects 实现按层级的费用归属:每个层级都有一个专用的 Bedrock 项目,并标记了费用分配元数据(CostCenter、Tier、Application)。项目 ID 通过 Bedrock Mantle 端点在每个推理请求中传递,因此所有模型调用费用在 AWS Cost Explorer 中会自动按层级进行分类。
# 每个层级的项目配置
TIER_PROJECTS = {
"basic": {
"name": "Healthcare-Basic",
"tags": {
"Application": "HealthcareDemo",
"Tier": "Basic",
"Environment": "demo",
"CostCenter": "HC-Basic",
},
},
"premium": {
"name": "Healthcare-Premium",
"tags": {
"Application": "HealthcareDemo",
"Tier": "Premium",
"Environment": "demo",
"CostCenter": "HC-Premium",
},
},
}在运行时,代理通过 Bedrock Mantle(OpenAI 兼容)端点在每次推理请求中传递项目 ID。这意味着每个模型调用都会自动标记上该层级的成本元数据:
# 项目 ID 在代理初始化时从 SSM 加载
project_id = get_ssm_parameter(config["project_ssm"])
self.model = OpenAIModel(
client_args={
"base_url": mantle_base_url,
"api_key": api_key,
"project": project_id, # 为每个推理调用标记成本归属
},
model_id=model_id,
)一旦你在 AWS Billing 中激活成本分配标签(标签可能需要最多 24 小时才能传播),你就可以在 AWS Cost Explorer 中按 CostCenter、Tier 或 Application 过滤和分组推理成本。这使你能够查看每个层级的成本情况。例如,可以比较运行 Ministral 3 8B Instruct 模型用于基础层级诊所的成本与运行 GPT OSS 120B 模型用于高级层级医院的成本。
通过结构化使用日志实现按诊所归因:Bedrock Projects 每个账户最多有 1,000 个,建议用于应用层级的边界。为了实现按诊所的成本粒度,解决方案会在每次代理调用后以结构化 JSON 格式记录令牌使用情况,其中租户上下文已经在系统中流动:
def _log_usage(self, result) -> None:
usage = result.metrics.accumulated_usage
logger.info(json.dumps({
"event": "inference_usage",
"tier": self.tier,
"clinic_id": self.clinic_id,
"user_id": self.user_id,
"model_id": self.model_id,
"input_tokens": usage.get("inputTokens", 0),
"output_tokens": usage.get("outputTokens", 0),
"total_tokens": usage.get("totalTokens", 0),
}))Strands SDK 会通过 AgentResult.metrics 对象在每次代理调用时自动跟踪令牌消耗(输入、输出和缓存指标)。通过将此信息与租户上下文中的 clinic_id 配对,每个日志条目都会将令牌使用情况归因于特定诊所。这些日志会存储在 CloudWatch 中,并可以使用 Logs Insights 查询以计算每个诊所的使用情况:
fields @timestamp, clinic_id, tier, model_id, input_tokens, output_tokens
| filter event = "inference_usage"
| stats sum(input_tokens) as total_input,
sum(output_tokens) as total_output,
count() as invocations
by clinic_id, tier
| sort total_output desc为了估算成本,你可以将令牌数量乘以每个模型的每令牌发布价格。
3. 通过 API Gateway 实现限速
每个层级的限速是通过 API Gateway 使用计划来执行的。解决方案为每个层级使用单独的使用计划,配置如下:
basic-tier-plan:
throttle: {rate_limit: 2, burst_limit: 5}
quota: {limit: 50, period: DAY}
premium-tier-plan:
throttle: {rate_limit: 10, burst_limit: 20}
quota: {limit: 500, period: DAY}清理
为了避免持续产生费用,当你不再需要这些资源时,可以删除已部署的资源。一个名为 cleanup.sh 的辅助脚本(位于 scripts/ 文件夹下)可用于帮助清理为本解决方案创建的资源。
结论
构建多租户 AI 应用需要特别关注数据隔离、服务差异化、成本归属和可扩展性。Amazon Bedrock AgentCore 通过原生平台功能,为满足这些需求提供了坚实的基础。此次实现的关键在于,多租户并不需要复杂的应用级隔离逻辑。通过结合 AWS 服务,如使用 Cognito 进行身份识别、S3 前缀进行数据隔离、API Gateway 进行速率限制、Bedrock Projects 和结构化日志进行成本归属,以及使用 Bedrock AgentCore 进行 AI 协调,你可以用最少的自定义代码构建安全、可扩展且成本效益高的多租户 AI 应用。你可以将这些模式应用于你正在构建的任何多租户代理应用中。
进一步阅读
- 在 GitHub 上查看完整的源代码
- 了解更多关于 Amazon Bedrock AgentCore 的信息
- 使用 Amazon Bedrock AgentCore 构建多租户代理
作者简介
'"`