Lore on AWS:用 Terraform 部署 NVMe 边缘缓存架构的高吞吐版本控制服务
Lore on AWS:用 Terraform 部署 NVMe 边缘缓存架构的高吞吐版本控制服务
Lore 是新一代开源版本控制系统,其 contrib/aws 示例 展示了一套面向游戏资产等大文件、高吞吐场景的 AWS 部署方案:以 Graviton c8gd.8xlarge 实例承载 ECS 任务,将 NVMe 实例存储作为本地分片缓存,S3 提供持久化存储,并通过边缘节点(Edge)+ 中心主节点(Primary)的两层架构实现接近亚毫秒级的 lore clone 读取。读完本文,你将掌握从镜像构建、Terraform 部署、客户端连接、验证测试到销毁清理的完整实操流程,并理解每一条关键配置背后的源码级实现。
架构总览:Primary + Edge 双层拓扑
该示例在 AWS 上构建了两层 Lore 服务节点,全部运行在 ECS on EC2 之上,核心组件如下:
| 组件 | 实例类型 | 职责 |
|---|---|---|
| Primary(ECS on EC2) | c8gd.8xlarge | Composite store:NVMe 缓存 + S3 持久化,向 Edge 提供复制服务 |
| Edge(ECS on EC2) | c8gd.8xlarge | Composite store:NVMe 缓存 + 复制型持久化(经 QUIC 连到 Primary),面向客户端 |
| Cloud Map DNS | — | 服务发现(primary.lore.internal、edge.lore.internal) |
| VPC | — | 私有子网、NAT、S3/DynamoDB 网关终端节点 |
| TLS CA | — | 自签名 CA,在节点与客户端之间建立信任 |
c8gd.8xlarge 是文档推荐实例类型:32 vCPU、64 GB 内存、1.9 TB NVMe 实例存储、25 Gbps 网络。NVMe 实例存储由 user_data.sh.tpl 在实例启动时检测并格式化为 xfs 文件系统(mkfs.xfs -f,挂载参数 noatime,nodiratime,discard),挂载到 /srv/urc,作为本地分片(fragment)缓存目录。S3 则提供持久化兜底,两者组合成"Composite store"。
这一设计直接呼应了文档末尾的说明:该示例实现了 Lore 系统设计中的架构——S3 作为不可变存储(immutable store)、DynamoDB 作为可变存储(mutable store)、边缘节点作为经 quics:// 复制到中心 Primary 的热缓存层。若需缩小实例规模做开发/测试,可在 terraform.tfvars.example 中设置 instance_type = "c8gd.xlarge"——同一架构、更小容量;而生产环境建议选用 32+ vCPU 且无网络带宽上限的实例规格。
数据流与端口设计
文档给出了一张简洁的数据流图,展示了客户端请求的完整路径:
Client ──lores://──→ Edge (NVMe cache hit → instant response)
│ cache miss
├──quics://41340───→ Primary (NVMe cache → S3 fallback)
└──lores://41337──→ Primary (branch resolution)
即:客户端总是连到 Edge;Edge 的 NVMe 缓存命中则立即返回;缓存未命中时,通过 quics:// 协议(41340 端口)向 Primary 复制数据,Primary 自身同样是"NVMe 缓存 → S3 兜底";分支解析则通过 lores:// 协议(41337 端口)代理到 Primary。
这些端口在 main.tf 中以 locals 明确定义,并与 lore-server/config/default.toml 保持一致:
| 端口 | 协议 | 用途 |
|---|---|---|
| 41337 | QUIC (UDP) + gRPC (TCP) | 客户端连接、分支解析 |
| 41339 | HTTP (TCP) | 健康检查、预签名 URL(presigned URL) |
| 41340 | QUIC (UDP) | Edge → Primary 内部复制 |
双服务的环境变量差异:理解"边缘代理"的钥匙
Primary 与 Edge 使用同一镜像,但通过 compute.tf 中不同的环境变量组合,实现了截然不同的角色:
- Primary(
aws_ecs_task_definition.lore):LORE__IMMUTABLE_STORE__MODE=composite,本地层LORE__IMMUTABLE_STORE__COMPOSITE__LOCAL__MODE=local,路径/srv/urc;持久化层...DURABLE__MODE=aws,直接读写 S3 与 DynamoDB;同时LORE__MUTABLE_STORE__MODE=aws、LORE__LOCK_STORE__MODE=aws,即分支指针与锁都落在 DynamoDB。 - Edge(
aws_ecs_task_definition.edge):本地缓存层同样指向/srv/urc(两者都取 80% 的 NVMe 容量,即1520000000000字节,预留 20% 给 xfs 元数据/日志);但持久化层...DURABLE__MODE=replicated,REMOTE_URL=quics://primary.lore.internal:41340;LORE__MUTABLE_STORE__MODE=remote,REMOTE_URL=lores://primary.lore.internal:41337——也就是说 Edge 把所有存储操作都代理回 Primary。
这正好解释了文档中"Edge 限制"一节:Edge 使用 remote mutable store 模式(代理到 Primary),因此 lore repository create、lore repository list 等管理命令必须直接连 Primary;而 clone、push、branch、merge、sync 等所有面向客户端的操作都可以经由 Edge 完成。与之配套,iam.tf 中 Edge 的任务角色(aws_iam_role.edge_task)刻意为空——它通过 gRPC/QUIC 代理所有存储操作,不需要任何直接的 S3/DynamoDB 权限。
冷启动自愈机制
文档特别指出:健康检查宽限期允许 Primary(120 秒)和 Edge(300 秒)完成初始化而不被误判为不健康;Edge 的重试配置可自动应对 Cloud Map DNS 传播延迟;首次部署时 Edge 节点可能重启 1~2 次,属预期且可自愈。
从 compute.tf 可以看到具体实现:Edge 容器的健康检查为 CMD-SHELL 探测 localhost:41339(echo > /dev/tcp/localhost/41339),startPeriod=120、interval=10、retries=6;复制型存储配置了 REGENERATE_RETRY(初始退避 100ms、最大退避 1000ms、最多 10 次)。注释说明:若冷部署时 Primary 尚未接受 QUIC,ReplicatedStore::new() 会阻塞导致端口无法绑定,超过 startPeriod + retries 后 ECS 会替换任务——此时 Primary 通常已就绪。e2e 测试脚本也对此做了兜底:检测到 Edge 卡住时执行 --force-new-deployment 强制重建。
前置条件与关键变量
环境要求
部署前需要准备(见 README 的 Prerequisites 一节):
- Terraform >= 1.7(versions.tf 同时锁定
hashicorp/awsprovider~> 6.0、hashicorp/tls>= 4.0、hashicorp/random>= 3.0) - 具备 VPC、ECS、EC2、S3、DynamoDB、IAM、Secrets Manager、Cloud Map、Auto Scaling、SSM 权限的 AWS 凭证
- Docker(用于构建 ARM64 容器镜像)
Terraform 变量
variables.tf 定义了全部输入变量,其中只有两个必填项:
| 变量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
container_image |
string | 无(必填) | Loreserver 容器镜像 URI(linux/arm64),必须是 v0.8.7 或更高版本(该版本读取本示例创建的 fragment state 表) |
allowed_cidrs |
list(string) | 无(必填) | 允许连接 Lore 的 CIDR(如 VPN 或办公网段) |
region |
string | us-west-2 |
AWS 区域 |
name |
string | lore |
所有资源的名称前缀 |
instance_type |
string | c8gd.8xlarge |
ECS 使用的 EC2 实例类型 |
fragment_metadata_table |
string | null |
旧版(v0.8.7 之前)fragment metadata 表名,仅升级场景使用(见下文升级章节) |
快速开始:从镜像到首个仓库
第 1 步:构建并推送容器镜像
在 Lore 仓库根目录执行(--platform linux/arm64 是关键,Graviton 是 ARM 架构):
docker buildx build --platform linux/arm64 -f lore-server/Dockerfile -t loreserver:v0.8.7 --load .
若在 x86 主机上构建,需要先注册 QEMU 以支持跨平台模拟:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
构建完成后务必验证镜像架构为 arm64——如果误用 x86 镜像,容器启动时会报 exec format error。然后推送到 ECR(替换 <ACCOUNT_ID> 与 <REGION>):
aws ecr get-login-password --region <REGION> | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com
aws ecr create-repository --repository-name loreserver --region <REGION>
docker tag loreserver:v0.8.7 <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/loreserver:v0.8.7
docker push <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/loreserver:v0.8.7
第 2 步:Terraform 部署
cd contrib/aws
cp terraform.tfvars.example terraform.tfvars
编辑 terraform.tfvars(完整示例见 terraform.tfvars.example):
region = "us-west-2"
container_image = "<ACCOUNT_ID>.dkr.ecr.us-west-2.amazonaws.com/loreserver:v0.8.7"
allowed_cidrs = ["10.0.0.0/8"] # 你的 VPC 或 VPN CIDR
# instance_type = "c8gd.8xlarge" # 默认值 — 32 vCPU, 64 GB, 1.9 TB NVMe, 25 Gbps
# fragment_metadata_table = "lore-metadata" # 仅当升级包含 v0.8.7 之前对象的栈时使用
然后执行:
terraform init
terraform apply
注意:首次 apply 可能需要运行第二次(DynamoDB PITR 存在时序竞争)。
第 3 步:客户端连接
所有服务都运行在私有子网中,客户端需要与 VPC 建立连通(如公有子网中的 NLB、AWS Client VPN、VPC peering 或堡垒机)。随后导出 CA 证书并合并到系统信任链:
terraform output -raw ca_certificate_pem > lore-ca.pem
cat /etc/ssl/certs/ca-certificates.crt lore-ca.pem > combined-ca.pem
export SSL_CERT_FILE=combined-ca.pem
创建仓库并完成首次资产导入:
lore repository create lores://edge.lore.internal:41337/my-game
lore clone lores://edge.lore.internal:41337/my-game ./my-game
cp /path/to/assets/* ./my-game/
cd my-game
lore stage .
lore commit "initial import"
lore push
在其他机器上克隆:
lore clone lores://edge.lore.internal:41337/my-game ./my-game
注意:edge.lore.internal 通过 VPC 内的 Cloud Map 私有 DNS 解析。该证书的生成细节在 tls.tf:Terraform 生成 ECDSA P384 的 CA 与服务器证书(有效期 8760 小时,SAN 覆盖 primary.*.internal、edge.*.internal、localhost),以 JSON 形式存入 Secrets Manager;ECS 任务中的 init-certs 边车容器(amazonlinux:minimal)在启动时从 Secrets Manager 拉取并写入共享卷 /certs,供 loreserver 容器挂载使用。CA 通过 outputs.tf 的 ca_certificate_pem 输出暴露给客户端。
基础设施拆解:每个 .tf 文件在做什么
网络层(network.tf)
network.tf 构建了一个最小化的 2-AZ 布局:10.0.0.0/16 VPC、公有/私有子网各 2 个、IGW + NAT、安全组与 S3/DynamoDB 网关终端节点(避免 AWS API 流量消耗 NAT 成本)。
安全组(aws_security_group.lore)的入站规则完整对应端口设计:
- 来自
allowed_cidrs的 41337 UDP(QUIC 客户端)、41337 TCP(gRPC 客户端)、41339 TCP(HTTP 健康检查/预签名 URL) - 安全组自引用(内部流量):41340 UDP(Edge→Primary 复制)、41337 TCP(分支解析 gRPC)、41337 UDP(数据传递 QUIC)
- 出站全放行
存储层(storage.tf)
storage.tf 创建 S3 桶与 4 张 DynamoDB 表,键结构均对齐 lore-aws/src/store/ 下的 Rust 实现:
| 资源 | 键结构 | 用途 |
|---|---|---|
S3 bucket(lore-fragments-*) |
— | 分片负载,开启版本化、AES256 服务端加密、公共访问封锁、7 天中止不完整分片上传 |
lore-fragments |
hash (B) + repository_context (B) | 分片关联(fragment associations) |
lore-fragment-state |
hash (B)(无排序键) | 分片状态:行存在即代表该 hash 存在,并携带 obliteration 状态 |
lore-mutable |
repository_id (B) + key (B) | 可变存储(分支指针) |
lore-locks |
hash (B) + repositoryBranch (B) + 3 个 GSI | 分布式锁(owner-repo-branch、repo-branch、repo-branch-description) |
全部表使用 PAY_PER_REQUEST 计费并开启 PITR。生产环境加固建议为每张表增加 deletion_protection_enabled = true(该示例为便于销毁而关闭)。
值得注意的是锁表的键结构及其 GSI 与 lore-aws 的 lock_store.rs 中的实现对应,而 LORE__PLUGINS__AWS__* 系列环境变量正是把这些表名与 S3 桶注入容器的通道(见 compute.tf 中 primary 容器的 environment 配置)。
计算层(compute.tf)
compute.tf 是整个示例的核心:
- 通过 SSM 参数获取 ECS 优化 AMI(Amazon Linux 2023,arm64);启动模板注入 user_data.sh.tpl(格式化 NVMe、配置 ECS agent);ASG 固定容量 2(min = max = desired),以便两个服务各占一台独立实例(
placement_constraints使用distinctInstance)。 - 关于容量提供方(capacity provider)有一处关键设计取舍:示例刻意不使用 ECS capacity provider,而是直接以
launch_type = "EC2"指向 ASG。代码注释解释:固定容量下托管伸缩/托管终止保护/托管排空毫无价值,且其自动化(逐实例 scale-in 保护、容量调和、终止生命周期钩子)会让terraform destroy死锁,导致 ECS 服务在 provider 的 20 分钟超时后仍卡在 DRAINING。若你将其改造为动态伸缩,需要添加aws_ecs_capacity_provider并把服务切换为capacity_provider_strategy。 - 两个任务定义均为
init-certs+loreserver双容器;loreserver将/srv/urc(instance-store-cache 卷)挂载为缓存目录,并注入完整的LORE__环境变量体系(TLS 证书路径、存储模式、AWS 插件表名、预签名 URL HMAC 密钥等)。 - Cloud Map 服务发现:
lore.internal私有命名空间下注册primary与edge两个 A 记录(TTL 10 秒,MULTIVALUE 路由策略),这正是客户端与节点间使用*.lore.internal域名的来源。 - 预签名 URL 功能所需的 HMAC 密钥由 iam.tf 中的
random_id(32 字节)生成并存入 Secrets Manager,通过LORE__SERVER__HTTP__PRESIGNED_URL_HMAC_KEY注入——这就是"Presigned URLs 已配置好"的底层实现。
IAM(iam.tf)
iam.tf 定义了 4 类角色:EC2 实例角色(ECS agent 通信 + SSM)、Primary 任务角色(S3 的 Get/Put/Delete/List + DynamoDB 的 Get/Put/Delete/Query/TransactWrite 等)、Edge 任务角色(刻意留空,见上文)、执行角色(拉镜像、写日志、读 Secrets Manager 中的 TLS 与 HMAC 密钥)。
部署验证
服务运行状态与日志检查:
aws ecs describe-services --cluster lore-cluster --services lore lore-edge \
--query 'services[].{name:serviceName,running:runningCount}' --region us-west-2
aws logs tail /ecs/lore --since 5m --region us-west-2
其中 lore-cluster、lore、lore-edge 这些名称均来自 Terraform 的 name 变量(默认 lore)与 outputs.tf 的输出(cluster_name、service_name、edge_service_name、primary_dns、edge_dns、s3_bucket、ca_certificate_pem 等)。
三层测试体系
contrib/aws/tests/ 目录提供了三层互补的验证手段:
| 文件 | 用途 |
|---|---|
| tests/plan.tftest.hcl | Terraform plan 级验证(离线、免费):mock provider + 断言(如集群/服务命名、存储键结构正确性),运行方式为 terraform test |
| tests/e2e-test.sh | 自动化端到端验证(需真实基础设施):./tests/e2e-test.sh us-west-2 |
| tests/TEST_PLAN.md | 手动演练手册:分阶段部署 + 验证 + 销毁的完整步骤 |
e2e 脚本的做法颇具参考价值:通过 SSM AWS-RunShellScript 在 ECS 实例上执行验证(因为实例在私有子网中),把测试负载脚本 e2e-payload.sh 上传到 S3 并预签名后下发;由于 Amazon Linux 2023 与 rust:latest 存在 GLIBC 不匹配,lore CLI 在 Docker 容器内以 --network host 方式运行。它验证的内容包括:
- 健康检查(Primary 与 Edge 均返回 HTTP 200)
- 经 Primary 的完整数据路径(create → stage → commit → push → clone)
- 经 Edge 的完整客户端工作流(clone → branch → merge → push → sync)
- 数据完整性(MD5 校验和在各路径间一致)
手动版验证(TEST_PLAN.md)还包含 AWS 资源级校验,并特别提醒:如果 Edge 因冷启动竞争卡住,可 force-new-deployment 强制重建。
定制与扩展速查表
文档的 Customize 一节给出了针对不同需求的修改指引:
| 需求 | 修改方式 |
|---|---|
| 缩小实例(开发/测试) | instance_type = "c8gd.xlarge" — 同一架构,容量更小 |
| 外部访问 | 在公有子网添加 NLB |
| 认证 | 设置 LORE__SERVER__AUTH__JWK__ENDPOINT(详见 Lore Server 配置参考) |
| 增加 Edge 节点 | 调大 ASG 的 min_size/max_size/desired_capacity 以及 edge 服务的 desired_count |
| 动态伸缩 | 添加带托管伸缩的 aws_ecs_capacity_provider(注意 compute.tf 中的容量注释) |
| 加速 Edge 启动 | 考虑在启动 loreserver 前增加一个轮询 primary.lore.internal 的启动探针 |
| 预签名 URL | 已通过 Secrets Manager 中的 HMAC 密钥配置完成 |
| 生产加固 | 为 DynamoDB 表添加 deletion_protection_enabled = true |
| 存在 v0.8.7 之前的对象 | 设置 fragment_metadata_table 指向既有元数据表(见下节) |
升级 v0.8.7 之前部署的注意事项
从 v0.8.7 起,分片元数据(fragment metadata)随 S3 对象本身携带,由 -fragment-state 表描述,而不再依赖旧的 -metadata 表。若你的栈是用本示例更早版本 apply 的,其中对象仍以旧方式写入,只有旧元数据表能描述它们。
当前示例不再创建该表,storage.tf 通过 removed 块将其从 Terraform 状态中移除但不销毁(destroy = false),避免数据丢失。恢复读取能力只需在 tfvars 中指向旧表:
fragment_metadata_table = "lore-metadata" # 你早期 apply 创建的表
服务器仅在对象自身不带元数据时才回退读取该表。一旦所有此类对象都被重写,就应取消该变量——此后无元数据的对象会被报告为损坏,而不会被可能张冠李戴的行记录描述。data "aws_dynamodb_table" 是查找而非资源:本示例永远不会创建这张表,若指定名字的表不存在,plan 会直接失败。
销毁与清理
S3 桶设置了 force_destroy = false(防止误删数据),完整销毁约需 5 分钟:
aws s3 rm s3://$(terraform output -raw s3_bucket) --recursive
terraform destroy
若销毁在 Cloud Map 服务处失败(报错 "Service contains registered instances"),说明 ECS 任务仍注册在服务发现中,需先缩容到零再销毁:
aws ecs update-service --cluster lore-cluster --service lore --desired-count 0 --region us-west-2
aws ecs update-service --cluster lore-cluster --service lore-edge --desired-count 0 --region us-west-2
sleep 30
terraform destroy
开发/测试环境若想一条命令完成销毁,可将 aws_s3_bucket 资源改为 force_destroy = true。
小结
这个示例的价值在于把 Lore 的分层存储模型(不可变分片 + 可变元数据 + 分布式锁)映射到了 AWS 托管服务之上:S3 负责持久化,DynamoDB 负责元数据与锁,NVMe 实例存储承担热缓存,Cloud Map 负责节点发现,而 Primary/Edge 双角色通过 QUIC 复制与远端可变存储代理优雅地实现了"边缘读缓存、中心写权威"的拓扑。无论是想要快速在 AWS 上跑起 Lore 做资产分发,还是希望理解其 Composite store 与 replicated store 的实际运作方式,contrib/aws 都是最直接的参考实现。