LocalStack S3、Moto 与 S3Mock 本地模拟工具手册
mock 通过后,为什么上线仍会失败
本地上传、下载都成功,真实 S3 却报 403;CI 偶发读到别的任务写入的对象;SDK 漏传 endpoint 后沿默认凭证链连到了 AWS。这些事故说明 S3 mock 的价值是快速、可重复地暴露应用错误,不是替真实云签发兼容证明。
三个工具适合不同半径:
| 工具 | 最有价值的现场 | 需要换真实环境验证的信号 |
|---|---|---|
| LocalStack S3 | S3 与 SQS、Lambda、SNS、IAM 等 AWS 服务联调 | IAM condition、KMS、Object Lock、跨账号、浏览器预签名 |
| Moto Server | Python 测试、轻量 HTTP mock、短生命周期 CI | 复杂 policy、共享服务、持久化与完整 S3 语义 |
| Adobe S3Mock | JVM、Spring、Testcontainers、CRUD 与 path-style | virtual-hosted、签名严谨性、KMS 与 notification |
如果系统只需要对象存储,不需要 AWS 周边服务,进程内 Moto 或 Testcontainers S3Mock 启动更快、隔离更自然。需要跨服务事件链时再选择 LocalStack。需要验证供应商权限、费用、审计和浏览器行为时,应使用隔离的真实开发桶。
锁定版本并准备本地端口
本机需要 Docker Engine / Docker Desktop、Docker Compose、AWS CLI v2,以及一个不含真实业务数据的测试文件。执行下面的检查,端口已占用时改 Compose 映射,不要结束未知进程:
docker compose version
lsof -i :4566
lsof -i :5000
lsof -i :9090
lsof -i :9191Windows 使用:
netstat -ano | findstr ":4566"
netstat -ano | findstr ":5000"
netstat -ano | findstr ":9090"
netstat -ano | findstr ":9191"LocalStack 使用 YYYY.MM.patch 日历版本。YYYY.MM 会随当月补丁移动,完整 patch 才是不可变锁定;版本升级前查 LocalStack changelog 和 镜像 tag 语义。下面使用已经锁定的 LocalStack 版本 2026.06.2、Moto 版本 5.2.2 和 S3Mock 版本 5.1.0,团队升级时只改变量并重跑契约套件。
OBJECT_STORE_PROVIDER=localstack
OBJECT_STORE_ENDPOINT=http://s3.localhost.localstack.cloud:4566
OBJECT_STORE_REGION=us-east-1
OBJECT_STORE_BUCKET=your-project-it
OBJECT_STORE_PREFIX=it/local/
OBJECT_STORE_FORCE_PATH_STYLE=false
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_EC2_METADATA_DISABLED=true
LOCALSTACK_VERSION=2026.06.2
LOCALSTACK_AUTH_TOKEN=YOUR_LOCALSTACK_AUTH_TOKEN
MOTO_VERSION=5.2.2
S3MOCK_VERSION=5.1.0假 access key 使用 test 这类明显无效值。LocalStack 会拒绝生产常见的 AKIA / ASIA 前缀以降低误用风险;凭证命名空间和校验规则见 LocalStack credentials。不要把真实 AWS profile、AK/SK、生产 bucket 或客户对象复制进测试进程。
启动 LocalStack S3
当前 LocalStack for AWS 镜像通过 auth token 激活,CI 需要 CI token;token 必须来自本地 secret 或 CI secret。授权方案和自动化使用条件可能变化,接入前在 LocalStack plans 与 Auth Token 文档 确认团队账号可用方式。
services:
localstack:
image: localstack/localstack-pro:${LOCALSTACK_VERSION:?set exact patch version}
container_name: your-project-localstack
ports:
- "127.0.0.1:4566:4566"
environment:
LOCALSTACK_AUTH_TOKEN: ${LOCALSTACK_AUTH_TOKEN:?inject auth token}
AWS_DEFAULT_REGION: ${OBJECT_STORE_REGION:-us-east-1}
OBJECT_STORE_BUCKET: ${OBJECT_STORE_BUCKET:-your-project-it}
SERVICES: s3
DEBUG: ${LOCALSTACK_DEBUG:-0}
PERSISTENCE: ${LOCALSTACK_PERSISTENCE:-0}
volumes:
- localstack-data:/var/lib/localstack
- ./localstack/init/ready.d:/etc/localstack/init/ready.d:ro
volumes:
localstack-data:SERVICES=s3 限制加载范围,降低本机资源占用;需要 SQS 或 Lambda 联调时再显式加入。PERSISTENCE=1 会把资源状态写到 /var/lib/localstack,适合个人跨重启调试;CI 更适合无状态启动,每次用 init hook 重建。
把 bucket 初始化写成 localstack/init/ready.d/10-s3.sh:
#!/usr/bin/env bash
set -euo pipefail
awslocal s3api head-bucket \
--bucket "${OBJECT_STORE_BUCKET:-your-project-it}" 2>/dev/null || \
awslocal s3api create-bucket \
--bucket "${OBJECT_STORE_BUCKET:-your-project-it}" \
--region "${AWS_DEFAULT_REGION:-us-east-1}"
echo "LocalStack S3 bucket ready: ${OBJECT_STORE_BUCKET:-your-project-it}"LocalStack 在 /etc/localstack/init/{boot.d,start.d,ready.d,shutdown.d} 的不同阶段运行 hook;创建 AWS 资源应放 ready.d。启动后检查 hook 状态和 S3 health,而不是靠固定等待十秒:
docker compose up -d
docker logs your-project-localstack --tail=100
curl -fsS http://127.0.0.1:4566/_localstack/healthMoto 和 S3Mock 何时更省事
Moto Server 适合测试进程外的轻量 HTTP mock:
services:
moto-s3:
image: motoserver/moto:${MOTO_VERSION:?set exact version}
ports:
- "127.0.0.1:5000:5000"
environment:
MOTO_PORT: 5000
S3_IGNORE_SUBDOMAIN_BUCKETNAME: "true"Moto 可能把本地域名第一段误识别为 bucket,path-style 场景可设置 S3_IGNORE_SUBDOMAIN_BUCKETNAME=true;支持到哪个 S3 API 应按 Moto S3 coverage 判断。假凭证、endpoint 和 region 必须在创建 SDK client 之前设置,因为 SDK 往往会缓存 provider chain。
S3Mock 更适合 JVM 测试随 Testcontainers 起停:
services:
s3mock:
image: adobe/s3mock:${S3MOCK_VERSION:?set exact version}
ports:
- "127.0.0.1:9090:9090"
- "127.0.0.1:9191:9191"
environment:
COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS: your-project-it
COM_ADOBE_TESTING_S3MOCK_STORE_REGION: us-east-19090 是 HTTP,9191 使用自签证书。测试 HTTPS 时应给测试客户端安装信任,不能把“关闭证书校验”带进生产代码。S3Mock 的支持矩阵和已知差异以 Adobe S3Mock 仓库 为准。
正向实验:上传、HEAD、下载、清理
LocalStack 推荐使用 S3 专用 endpoint s3.localhost.localstack.cloud:4566;它支持 virtual-hosted style。若企业 DNS 无法解析该域名,改用 http://localhost:4566 并开启 path-style,具体 URL 形态见 LocalStack S3 文档。
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_EC2_METADATA_DISABLED=true
export OBJECT_STORE_ENDPOINT=http://s3.localhost.localstack.cloud:4566
export OBJECT_STORE_BUCKET=your-project-it
export OBJECT_STORE_PREFIX="it/${CI_JOB_ID:-local}/"
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3api head-bucket \
--bucket "$OBJECT_STORE_BUCKET"
printf 'hello localstack s3\n' > hello.txt
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3 cp \
hello.txt "s3://${OBJECT_STORE_BUCKET}/${OBJECT_STORE_PREFIX}hello.txt"
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3api head-object \
--bucket "$OBJECT_STORE_BUCKET" \
--key "${OBJECT_STORE_PREFIX}hello.txt"
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3 cp \
"s3://${OBJECT_STORE_BUCKET}/${OBJECT_STORE_PREFIX}hello.txt" downloaded.txt
cmp hello.txt downloaded.txt
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3 rm \
"s3://${OBJECT_STORE_BUCKET}/${OBJECT_STORE_PREFIX}hello.txt"
if aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3api head-object \
--bucket "$OBJECT_STORE_BUCKET" \
--key "${OBJECT_STORE_PREFIX}hello.txt"; then
echo "cleanup failed" >&2
exit 1
fi可接受的证据是:head-object 返回 Size/ETag,cmp 退出码为 0,删除后的 head-object 返回 404。Moto 使用 http://127.0.0.1:5000,S3Mock 使用 http://127.0.0.1:9090;切换时保持命令和对象 key 不变,差异才可比较。S3Mock 只支持 path-style,SDK 切到它时必须把 OBJECT_STORE_FORCE_PATH_STYLE 改为 true,否则请求会落到不受支持的 bucket 子域名。
反向实验:让误连在发请求前失败
“使用假凭证”还不够,SDK 漏传 endpoint 时仍会尝试访问真实 AWS。测试启动脚本应先验证 provider、host 和凭证形态:
case "${OBJECT_STORE_PROVIDER}:${OBJECT_STORE_ENDPOINT}" in
localstack:http://*.localhost.localstack.cloud:4566|localstack:http://localhost:4566|\
moto:http://127.0.0.1:5000|s3mock:http://127.0.0.1:9090)
;;
*)
echo "refuse unsafe object-store endpoint: ${OBJECT_STORE_ENDPOINT:-unset}" >&2
exit 64
;;
esac
case "${AWS_ACCESS_KEY_ID:-}" in
test|local|dummy) ;;
*) echo "refuse non-test access key" >&2; exit 65 ;;
esac把 endpoint 改成空值或真实 AWS host,脚本应以 64 退出且没有网络请求;把 access key 换成 AKIA...,应以 65 退出。这条反向证据比在失败后查看账单更可靠。应用启动日志只打印 provider、endpoint host、region、bucket 和 prefix,不能打印 secret、token 或预签名 URL。
Java SDK v2 接入
业务代码不应出现固定的 localhost。用同一组配置创建客户端:
S3Configuration s3Config = S3Configuration.builder()
.pathStyleAccessEnabled(forcePathStyle)
.build();
S3Client client = S3Client.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(s3Config)
.build();预签名器必须复用相同 endpoint、region、凭证和 S3Configuration:
S3Presigner presigner = S3Presigner.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(s3Config)
.build();SDK 契约测试固定执行 PutObject -> HeadObject -> GetObject -> DeleteObject -> HeadObject(404)。每个 run 使用 it/<job-id>/<random-id>/;测试结束只删除自己的 prefix。LocalStack 开启持久化时,还要在新旧状态下各跑一次,避免用历史对象掩盖初始化缺陷。
mock 和真实云要分层验收
LocalStack 默认关闭 IAM enforcement,S3 预签名和 KMS 校验也有可调开关。打开这些开关能提高本地严格度,却仍不等于 AWS 控制面、账号边界和密钥服务。相关配置的默认值与版本要求应从 LocalStack configuration 获取。
| 能力 | 快速回归 | 必须补的真实开发桶证据 |
|---|---|---|
| CRUD / HEAD / 分页 | 三种 mock 均可 | SDK 与目标厂商错误码差异 |
| 预签名 GET / PUT | LocalStack 优先 | 浏览器 CORS、域名、TLS、过期、header |
| bucket policy / IAM | LocalStack 可做早期反馈 | 最小权限身份允许与拒绝用例 |
| KMS / Object Lock | 只验证请求结构 | 真 KMS 权限、retention、legal hold |
| notification / event | LocalStack 联调 | 重试、重复、乱序、失败队列和审计 |
| virtual-hosted style | LocalStack DNS 测试 | 真实域名、证书、代理和厂商 endpoint |
预签名 URL 必须用浏览器或等价 HTTP 链路发起实际 GET/PUT,并在过期后验证拒绝。Moto 或 S3Mock 接受请求,不代表它们完整校验签名、method 和过期时间。
并行、容量和成本不是 mock 的空白区
共享 mock 的首要容量风险是旧对象、旧 bucket、旧 multipart 和持久化快照不断累积。每个 job 使用唯一 prefix,并记录对象数、总字节数、未完成 multipart 数量和最长存活时间。大文件测试要覆盖 multipart 中断与 abort;只验证成功合并会漏掉最昂贵的清理路径。
LocalStack 成本由镜像订阅、CI 分钟、CPU/内存、持久卷和维护时间组成;真实开发桶成本还包括请求次数、存储、版本、出站流量、日志审计和 KMS。选择工具时比较“每次测试启动一个隔离实例”与“共享长驻实例”的总成本:前者启动慢但污染少,后者摊薄资源却需要 owner、配额、清理和并发治理。
生产数据、真实手机号、邮箱、订单号、合同号和客户文件不能为了逼真直接进入 mock。测试夹具应脱敏或合成,日志与测试报告隐藏内部域名、bucket、完整 key、auth token 和预签名 URL。
从现象回到配置
SDK 打到真实 AWS
若日志出现真实 region、账号 ID、AccessDenied 或费用告警,立即停止测试并吊销可能暴露的凭证。检查 endpoint 注入、AWS_PROFILE、环境变量、共享 credentials 文件和实例元数据;修复后先跑 endpoint 守卫的反向用例,再恢复 CRUD。
LocalStack 重启后数据丢失或残留
检查 PERSISTENCE 和 /var/lib/localstack volume 是否一致。CI 使用无状态实例加 ready hook;个人持久化实例提供明确 reset 动作。LocalStack 的状态快照有版本兼容规则,升级前按 Persistence 文档 验证,不把 snapshot 当成长期稳定数据格式。
并行测试读到错误内容
列出失败对象 key,检查是否包含 job id 和随机 run id。若多个 job 共用固定 it/local/,先修命名与清理边界;不要在全局 before hook 删除整桶,这会把污染问题变成互相误删。
本地预签名成功,浏览器失败
比较实际 URL host、method、签名 header、CORS、TLS、系统时间和有效期。LocalStack 生成成功只证明本地链路;真实开发桶的浏览器契约必须是发布门禁。
S3Mock HTTPS 握手失败
9191 使用自签证书。开发测试可以把对应 CA 加入专用 trust store;只需 CRUD 时使用 9090。禁止在共享 SDK 工厂里关闭证书校验,否则测试配置可能泄漏到生产。
清理、重置和回滚
先列出当前 run 的对象:
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3 ls \
"s3://${OBJECT_STORE_BUCKET}/${OBJECT_STORE_PREFIX}" --recursive确认 bucket、prefix 和 owner 后再删除:
aws --endpoint-url "$OBJECT_STORE_ENDPOINT" s3 rm \
"s3://${OBJECT_STORE_BUCKET}/${OBJECT_STORE_PREFIX}" --recursive测试独占 bucket 时才能删桶;共享 bucket 永远按 prefix 清理。LocalStack 个人环境需要完全重置时,先停服务并确认 volume 名属于当前项目:
docker compose down
docker volume ls --filter name=localstack
docker compose down -v-v 会删除持久化状态,不能用于共享 CI 或团队实例。版本升级回滚时保留旧镜像 tag、Compose 配置和可丢弃的测试快照;先用新建无状态实例验证旧契约,再替换共享实例。mock 数据本就应可重建,不要把它升级成需要备份恢复的事实来源。
团队治理门槛
团队应登记工具、完整版本、endpoint、启用服务、bucket、prefix 规则、owner、初始化脚本、持久化策略、资源上限、auth token owner、清理期限和真实云复核项。每次版本升级同时运行 mock 契约和真实开发桶契约,并记录两者差异,避免把本地宽松行为写进生产假设。
进入主分支前至少满足:
完整 patch 版本已锁定,未使用 dev;镜像授权与 token 类型适合本地或 CI 场景。endpoint 守卫能拒绝空值、真实云 host 和非测试 access key。上传、HEAD、下载、删除和删除后 404 都有断言。
每个 job 使用独立 prefix,清理脚本先 list 再删且不碰整桶。SDK 与 presigner 复用 endpoint、region、凭证和 path-style 配置。预签名、IAM、KMS、Object Lock、notification 和 virtual-hosted style 有真实开发桶复核路径。
持久化状态可重建,multipart、旧对象和测试快照有容量与清理期限。日志、报告和截图不泄露真实凭证、token、内部 endpoint、对象标识或预签名 URL。
