多租户架构的老问题:隔离做得太粗,一个租户的故障拖垮所有人;做得太细,每个租户单独开 AWS 账号,运维成本直线上升。AWS 最近分享了一种"混合"思路——在少数几个 AWS 账号内,用 ALB 规则、独立 ECS 集群和 PrivateLink 拼出接近账号级隔离的效果,同时把共享依赖集中管理。下面拆开看每个环节怎么做,并给出可直接改造的配置示例。
隔离光谱上的折中位置
多租户隔离通常分三档:
| 模式 | 隔离强度 | 运维复杂度 | 典型场景 |
|---|---|---|---|
| Silo(每租户独立账号) | 最强 | 最高 | 金融、医疗合规 |
| Hybrid(共享账号 + 独立计算) | 中强 | 中等 | SaaS 平台、有状态服务 |
| Pool(全共享) | 最弱 | 最低 | 内部工具、轻量 API |
混合模式的核心思路:网络层和计算层按租户隔离,数据层和控制面共享。这样既避免了一个租户的 ECS 任务挤占另一个的 CPU / 内存,又不用为每个租户维护一整套 VPC、IAM 和监控体系。
Route 53 加权路由:跨账号流量分配
当你在多个 AWS 账号间部署同一服务的不同租户实例时,第一步是把流量正确分发到对应账号。
Route 53 加权路由(Weighted Routing)配合多账号 DNS 记录,实现按权重或按租户标签分流:
# 在主账号中创建托管区(假设域名 saas.example.com)
aws route53 create-hosted-zone \
--name saas.example.com \
--caller-reference "tenant-routing-$(date +%s)"
# 为租户 A(账号 111111111111)创建加权记录
aws route53 change-resource-record-sets \
--hosted-zone-id Z1PA6795UKMFR9 \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "app-a.saas.example.com",
"Type": "A",
"AliasTarget": {
"DNSName": "alb-account-a.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true,
"HostedZoneId": "Z215JYRZR1E"
},
"Weight": 100
}
}]
}'
# 为租户 B(账号 222222222222)创建另一条加权记录
aws route53 change-resource-record-sets \
--hosted-zone-id Z1PA6795UKMFR9 \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "app-b.saas.example.com",
"Type": "A",
"AliasTarget": {
"DNSName": "alb-account-b.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true,
"HostedZoneId": "Z215JYRZR1E"
},
"Weight": 100
}
}]
}'
关键点:每条记录指向不同账号的 ALB,EvaluateTargetHealth 开启后,如果某个账号的 ALB 健康检查失败,Route 53 自动把流量切走——这比手工切换快得多。
如果租户数量多但不需要跨账号,也可以在单账号内用 简单路由 + ALB 规则 替代加权路由,后面会看到。
ALB 监听器规则:租户级路由的精细控制
流量到了 ALB 之后,需要按租户标识路由到对应的 ECS 目标组。ALB 监听器规则支持按 Host header、Path、Query string 甚至自定义 Header 匹配,这正好对应不同的租户识别策略。
# 创建 ALB 监听器(假设 ALB 已存在)
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111111111111:loadbalancer/app/hybrid-alb/50dc6c495ef0ad96 \
--protocol HTTPS \
--port 443 \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/default-tg/1234567890
# 为租户 A 添加基于 Host header 的路由规则
aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:111111111111:listener/app/hybrid-alb/50dc6c495ef0ad96/abcd1234 \
--priority 1 \
--conditions Field=host-header,Values='tenant-a.saas.example.com' \
--actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/tenant-a-tg/9876543210
# 为租户 B 添加基于自定义 Header 的路由规则
aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:111111111111:listener/app/hybrid-alb/50dc6c495ef0ad96/abcd1234 \
--priority 2 \
--conditions Field=http-header,HttpHeaderConfig={HeaderName=X-Tenant-Id,Values='tenant-b'} \
--actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/tenant-b-tg/5678901234
两种常见租户识别方式:
- 子域名模式:
tenant-a.saas.example.com→ Host header 匹配。适合租户有独立入口的场景。 - Header / Path 模式:
X-Tenant-Id: tenant-b或/b/api/...→ Header / Path 匹配。适合统一入口、API 网关后端。
优先级数字越小越先匹配,确保租户规则排在默认规则前面。
每租户独立 ECS 集群:计算层隔离的核心
共享 ECS 集群里,一个租户的 CPU 密集型任务会挤压其他租户——这对有状态服务尤其致命(数据库连接池、缓存预热、长连接都在争资源)。混合架构的做法:每个租户一个 ECS 集群,共享账号内隔离计算资源。
# 为租户 A 创建独立 ECS 集群
aws ecs create-cluster \
--cluster-name tenant-a-cluster \
--tags key=TenantId,value=tenant-a
# 为租户 B 创建独立 ECS 集群
aws ecs create-cluster \
--cluster-name tenant-b-cluster \
--tags key=TenantId,value=tenant-b
集群创建后,用 Capacity Provider 把 Auto Scaling Group 绑定到对应集群,实现按租户独立扩缩容:
# 创建 ASG 并关联到租户 A 的集群
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name tenant-a-asg \
--min-size 2 --max-size 10 --desired-capacity 3 \
--vpc-zone-identifier "subnet-aaa1,subnet-aaa2" \
--launch-template LaunchTemplateId=lt-0abc1234,Version=1
# 创建 Capacity Provider 并关联
aws ecs create-capacity-provider \
--name tenant-a-cp \
--auto-scaling-group-provider autoScalingGroupArn=arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:tenant-a-asg,managedScaling=status=ENABLED,targetCapacityPercent=80
# 把 Capacity Provider 关联到集群
aws ecs put-cluster-capacity-providers \
--cluster tenant-a-cluster \
--capacity-providers tenant-a-cp \
--default-capacity-provider-strategy capacityProvider=tenant-a-cp,weight=1,base=1
这样租户 A 的任务只跑在租户 A 的 EC2 实例上,扩缩容也只看租户 A 的负载。租户 B 完全独立,互不干扰。
PrivateLink:共享依赖的安全通道
有状态服务通常依赖数据库、缓存、消息队列等共享组件。这些组件放在一个"共享服务"账号里,各租户账号通过 AWS PrivateLink(VPC Endpoint Service)访问,流量全程走 AWS 内网,不经过公网。
# 在共享服务账号中:为 RDS / ElastiCache 创建 NLB(PrivateLink 要求 NLB 作为前端)
aws elbv2 create-load-balancer \
--name shared-services-nlb \
--type network \
--subnets subnet-shared1 subnet-shared2
# 创建 VPC Endpoint Service,暴露给租户账号
aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns arn:aws:elasticloadbalancing:us-east-1:999999999999:loadbalancer/net/shared-services-nlb/abc123 \
--acceptance-required True \
--private-dns-name shared-db.saas.internal
# 在租户 A 账号中:创建 Interface VPC Endpoint 连接到共享服务
aws ec2 create-vpc-endpoint \
--vpc-endpoint-type Interface \
--vpc-id vpc-tenant-a \
--service-name com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef0 \
--subnet-ids subnet-aaa1 subnet-aaa2 \
--security-group-ids sg-tenant-a-endpoint
--acceptance-required True 意味着共享服务账号需要手动批准连接请求,这给了你一层访问控制——只有审核通过的租户才能连上来。
# 共享服务账号:批准租户 A 的连接请求
aws ec2 accept-vpc-endpoint-connections \
--service-id vpce-svc-0123456789abcdef0 \
--vpc-endpoint-ids vpce-0aaa1111bbb2222
PrivateLink 的好处:租户 VPC 不需要和共享 VPC 做 VPC Peering,没有 CIDR 冲突风险,也不需要路由表维护。每个租户只看到自己被授权的服务端点。
一套可改造的 Terraform 示例
把上面各环节串起来,以下是一个最小可运行的 Terraform 模块骨架。实际部署前需要替换 var 中的 VPC ID、子网 ID 和镜像地址。
# tenant-ecs-cluster.tf — 每租户独立 ECS 集群 + ALB 规则 + PrivateLink
variable "tenant_id" {
description = "租户标识,如 tenant-a"
type = string
}
variable "vpc_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
variable "shared_vpce_service_name" {
description = "共享服务的 VPC Endpoint Service 名称"
type = string
}
variable "alb_listener_arn" {
type = string
}
variable "container_image" {
description = "有状态服务的容器镜像"
type = string
}
# ── ECS 集群 ──
resource "aws_ecs_cluster" "tenant" {
name = "${var.tenant_id}-cluster"
tags = { TenantId = var.tenant_id }
}
# ── 目标组 ──
resource "aws_lb_target_group" "tenant" {
name = "${var.tenant_id}-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/health"
matcher = "200"
}
}
# ── ALB 监听器规则:按 Host header 路由 ──
resource "aws_lb_listener_rule" "tenant" {
listener_arn = var.alb_listener_arn
priority = 10 # 按租户数量分配不同优先级
condition {
host_header {
values = ["${var.tenant_id}.saas.example.com"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.tenant.arn
}
}
# ── ECS 任务定义 ──
resource "aws_ecs_task_definition" "stateful_app" {
family = "${var.tenant_id}-stateful-app"
network_mode = "awsvpc"
requires_compatibilities = ["EC2"]
cpu = "512"
memory = "1024"
container_definitions = jsonencode([
{
name = "app"
image = var.container_image
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "TENANT_ID", value = var.tenant_id },
{ name = "DB_HOST", value = "shared-db.saas.internal" } # PrivateLink 私有域名
]
# 有状态服务建议设置较长的健康检查间隔
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60 # 给预热留足时间
}
}
])
}
# ── PrivateLink Endpoint:连接共享数据库 ──
resource "aws_vpc_endpoint" "shared_db" {
vpc_id = var.vpc_id
service_name = var.shared_vpce_service_name
vpc_endpoint_type = "Interface"
subnet_ids = var.subnet_ids
security_group_ids = [aws_security_group.endpoint.id]
}
resource "aws_security_group" "endpoint" {
name = "${var.tenant_id}-vpce-sg"
vpc_id = var.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_id] # 仅允许 VPC 内流量
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
使用方式:
# 为租户 A 部署
terraform apply \
-var='tenant_id=tenant-a' \
-var='vpc_id=vpc-0aaa1111' \
-var='subnet_ids=["subnet-aaa1","subnet-aaa2"]' \
-var='shared_vpce_service_name=com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef0' \
-var='alb_listener_arn=arn:aws:elasticloadbalancing:us-east-1:111111111111:listener/app/hybrid-alb/50dc6c495ef0ad96/abcd1234' \
-var='container_image=123456789012.dkr.ecr.us-east-1.amazonaws.com/stateful-app:v1.2'
# 为租户 B 部署(换变量即可)
terraform apply \
-var='tenant_id=tenant-b' \
-var='vpc_id=vpc-0bbb2222' \
...
每个租户一次 apply,产出独立的集群、目标组、路由规则和 PrivateLink 连接。新增租户就是再跑一次,删除租户就是 terraform destroy。
采纳建议与取舍清单
混合多租户不是万能方案,以下几条帮助判断是否适合你的场景:
适合的情况: - 有状态服务(数据库连接池、长生命周期缓存、WebSocket)对资源争抢敏感 - 租户数量在 10–100 之间,开独立账号太贵,全池化太危险 - 合规要求"逻辑隔离"但不强制"物理账号隔离"
不适合的情况: - 租户少于 5 个——直接开独立账号,运维负担可控,隔离更彻底 - 租户超过 500 个——独立 ECS 集群的管理成本会反噬,考虑回到池化 + 资源配额(cgroup / ECS task CPU/memory limits) - 无状态 API 服务——池化 + 请求级隔离足够,没必要为每个租户开集群
落地前的检查项:
- [ ] Route 53 健康检查已开启
EvaluateTargetHealth,故障自动切流 - [ ] ALB 规则优先级有明确分配策略,避免新租户规则意外覆盖旧规则
- [ ] 每个 ECS 集群绑定了独立的 Capacity Provider 和 ASG,扩缩容不串
- [ ] PrivateLink 连接的
acceptance-required设为True,共享服务审批后才能接入 - [ ] 有状态服务的 ECS 任务定义中
startPeriod足够长(≥ 60s),避免预热期被误判为不健康 - [ ] 监控和告警按
TenantIdtag 分维度,一个租户的延迟飙升不会淹没全局视图 - [ ] CI/CD 流水线支持按租户参数化部署,新增租户不需要改代码
混合架构的本质是在隔离光谱上找一个可运营的中间点。它用 ECS 集群隔离计算、ALB 规则隔离流量、PrivateLink 隔离网络路径,同时保留共享账号的运维效率。如果你正在为"开账号太贵、全共享太险"的两难找出路,这套组合值得试一把。