LocalStack 多 AWS 服务本地模拟与项目接入工具手册
S3 上传成功,SQS 消费为什么仍然失败
订单服务在本地把文件写进 S3 后返回成功,异步消费者却一直收不到消息。排查结果并不是“LocalStack 没启动”:S3 客户端显式连接了 4566 gateway,SQS 客户端却沿用 SDK 默认 endpoint;另一组测试拿到了含 localhost 的 Queue URL,随后把它传给 Compose 内的应用容器,容器把 localhost 解释成自己;还有一次 CI 复用了持久化 volume,旧队列让初始化脚本误以为环境已经正确。
LocalStack 的价值在于把 AWS API 调用搬进开发机和 CI 的快速反馈环,而不是把所有 AWS 行为变成同一个本地进程。请求先进入 gateway,再由服务 provider 处理;资源状态可能来自 init hook、当前测试或持久化快照;SDK 的 region、endpoint、凭证和寻址方式共同决定请求究竟去了本地、真实 AWS,还是一个无法解析的地址。
团队基线锁定 LocalStack CalVer 版本 2026.06.3,格式为 YYYY.MM.patch;官方从 4.14.0 之后切换到该格式,版本线与发行说明可在 Changelog 核对。镜像标签不能混为一谈:当前 latest 与 stable 只跟随正式发布,YYYY.MM 会在当月补丁发布时移动,YYYY.MM.patch 才是不可变的精确版本,dev 才跟随主分支未发布变更。开发模板和 CI 应固定补丁标签或 digest;需要自动接收补丁时,也应通过依赖更新任务提交并重跑验证,而不是让运行环境静默漂移。
许可先于部署决策。LocalStack for AWS 的商业使用计划为 Base、Ultimate、Enterprise,Hobby 面向非商业使用,另有特定资格计划;工作区、用户许可、自动化或共享基础设施的使用方式会影响授权与用量分配,具体条款以 LocalStack Plans 为准。使用 for AWS / Pro 能力需要 LOCALSTACK_AUTH_TOKEN,CI 必须使用 CI Auth Token,不能复用开发者 token。计划页只说明服务是否包含在订阅中,不代表每个 API 和行为都已实现;采购和升级前还要逐项核对目标服务页的 API coverage、已知限制及 persistence coverage,不能仅按“能启动容器”判断可用性。
从 gateway 启动一套可重建环境
开发机需要 Docker、Docker Compose、AWS CLI 和一个开发者 Auth Token。Windows 使用 Docker Desktop;Linux CI runner 还要确认 Docker socket 权限。只验证 S3/SQS 时不必挂载 Docker socket;Lambda、ECS、EKS 等需要 LocalStack 再启动容器的服务才需要它,而 socket 等价于高权限宿主机控制入口。
.env 只在本机保存真实 token,并加入忽略规则;提交到仓库的示例只能保留空占位:
LOCALSTACK_VERSION=2026.06.3
LOCALSTACK_AUTH_TOKEN=
LOCALSTACK_PORT=4566
AWS_DEFAULT_REGION=us-east-1Compose 将 gateway 只绑定到回环地址:
services:
localstack:
image: localstack/localstack-pro:${LOCALSTACK_VERSION:?set LOCALSTACK_VERSION}
ports:
- "127.0.0.1:${LOCALSTACK_PORT:-4566}:4566"
environment:
LOCALSTACK_AUTH_TOKEN: ${LOCALSTACK_AUTH_TOKEN:?set LOCALSTACK_AUTH_TOKEN}
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1}
DEBUG: ${LOCALSTACK_DEBUG:-0}
PERSISTENCE: ${LOCALSTACK_PERSISTENCE:-0}
SQS_ENDPOINT_STRATEGY: ${SQS_ENDPOINT_STRATEGY:-standard}
volumes:
- ./localstack/init:/etc/localstack/init:ro
- localstack-data:/var/lib/localstack
volumes:
localstack-data:4566 是 AWS API 与内部诊断接口的统一 gateway。宿主机使用 http://localhost.localstack.cloud:4566 或 http://localhost:4566;同一 Compose 网络里的应用使用 http://localstack:4566。域名受企业 DNS rebind protection 影响时,使用 localhost 并让 S3 SDK 强制 path style。跨容器调用不能把宿主机的 localhost 原样复制过去,官方的 endpoint 网络指南给出了不同网络位置的地址选择。
先启动并观察 gateway:
docker compose up -d localstack
curl -fsS http://localhost.localstack.cloud:4566/_localstack/health
curl -fsS http://localhost.localstack.cloud:4566/_localstack/init/ready
curl -fsS http://localhost.localstack.cloud:4566/_localstack/infohealth 说明 gateway 与服务状态,init endpoint 说明初始化阶段是否完成,info 中的 is_license_activated 应为 true。这三项分别回答“进程是否可达、资源初始化是否完成、许可是否激活”,不能互相替代。测试只能在 ready 成功且许可已激活后开始。启动失败先看 docker compose logs localstack;token 无效、volume 权限、端口占用和 hook 解析错误会留下比 SDK 超时更直接的证据。
init hooks 负责确定性,不负责藏历史
LocalStack 依次提供 /etc/localstack/init/boot.d、start.d、ready.d、shutdown.d。资源创建通常放进 ready.d,因为此时 AWS API 已可调用。创建 localstack/init/ready.d/10-bootstrap.sh:
#!/usr/bin/env bash
set -euo pipefail
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
BUCKET="te-order-dev-files"
QUEUE="te-order-dev-payment-events"
if ! awslocal s3api head-bucket --bucket "$BUCKET" 2>/dev/null; then
awslocal s3api create-bucket --bucket "$BUCKET" --region "$REGION"
fi
if ! awslocal sqs get-queue-url --queue-name "$QUEUE" >/dev/null 2>&1; then
awslocal sqs create-queue --queue-name "$QUEUE" >/dev/null
fi
printf 'LocalStack resources ready: bucket=%s queue=%s\n' "$BUCKET" "$QUEUE"脚本必须是 LF 换行并带可执行位。Windows 仓库可在 CI 构建上下文中显式 chmod +x,但不要靠开发者手工进入容器修复。hook 应幂等:本地启用持久化时资源可能已经存在,CI 从空状态启动时又必须完整创建。set -euo pipefail 会把任一创建失败传递给 init 状态,避免后续测试在半初始化环境里运行。
正向检查:
curl -fsS http://localhost.localstack.cloud:4566/_localstack/init/ready
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url=http://localhost.localstack.cloud:4566 \
s3api head-bucket --bucket te-order-dev-files预期 init endpoint 表示 ready,head-bucket 退出码为 0。反向检查可以把脚本中的 bucket 名改成不合法值后,用空 volume 启动;预期 hook 失败,init 状态不再是 ready,容器日志包含对应 S3 API 错误。修复脚本、删除该实验 volume 并重建后才恢复。这个实验验证的是“初始化失败会阻断测试”,不是 AWS 本身的全部校验规则。
S3 与 SQS 的可复制闭环
先给当前 shell 注入明确的本地身份:
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_SESSION_TOKEN=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_EC2_METADATA_DISABLED=true
export LOCALSTACK_ENDPOINT=http://localhost.localstack.cloud:4566这些是假凭证,但 AWS SDK 仍要求签名所需字段存在。AWS_EC2_METADATA_DISABLED=true 防止凭证链在缺失环境变量时探测实例元数据。不要在开发机使用真实 AKIA / ASIA 风格 access key;一旦 endpoint 漏配,真实凭证会把一个本地测试错误升级成云资源和费用事故。
执行 S3 写入、SQS 发送与接收:
printf 'order=o-1001\n' > localstack-smoke.txt
aws --endpoint-url="$LOCALSTACK_ENDPOINT" s3 cp \
localstack-smoke.txt \
s3://te-order-dev-files/orders/o-1001.txt
aws --endpoint-url="$LOCALSTACK_ENDPOINT" s3api head-object \
--bucket te-order-dev-files \
--key orders/o-1001.txt
QUEUE_URL="$(aws --endpoint-url="$LOCALSTACK_ENDPOINT" sqs get-queue-url \
--queue-name te-order-dev-payment-events \
--query QueueUrl --output text)"
aws --endpoint-url="$LOCALSTACK_ENDPOINT" sqs send-message \
--queue-url "$QUEUE_URL" \
--message-body '{"orderId":"o-1001","event":"payment.requested"}'
aws --endpoint-url="$LOCALSTACK_ENDPOINT" sqs receive-message \
--queue-url "$QUEUE_URL" \
--wait-time-seconds 1 \
--message-attribute-names All \
--attribute-names All预期 head-object 返回对象元数据,send-message 返回 MessageId,receive-message 的 Messages 数组包含刚才的 JSON。测试若要证明消费完成,还要使用返回的 ReceiptHandle 调用 delete-message,再轮询到队列不可见消息数回到基线;只看到一次 body 不能证明 ack 路径正确。
S3 有 path-style 与 virtual-hosted-style 两种寻址。使用 http://localhost:4566 时,SDK 通常要设置 forcePathStyle=true;使用 http://s3.localhost.localstack.cloud:4566 可走更接近 AWS 的 host-style。地址选择与 SDK 字段见 LocalStack S3。常见反例是 SDK 自动请求 <bucket>.localhost:4566,而 DNS 或 Host 解析不符合预期,最终表现为连接错误或找不到 bucket。
SQS 返回的 Queue URL 本身也是后续请求地址。SQS_ENDPOINT_STRATEGY=standard 更接近 AWS,并编码 region 与 account,适合宿主机直接调用;应用运行在其他容器中时,官方建议设置可从该容器解析的 LOCALSTACK_HOST,并使用 SQS_ENDPOINT_STRATEGY=path。宿主机与 Compose 容器是两个网络命名空间,一条带主机名的 Queue URL 未必能同时服务两边;先确定实际调用方,再按 LocalStack SQS选择策略。不要在应用里拆 Queue URL 字符串再拼接,保存逻辑名称并让 SDK 通过 GetQueueUrl 获取。
反向实验:让 endpoint 错误立即可见
先把 endpoint 改到未监听端口:
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
AWS_DEFAULT_REGION=us-east-1 \
aws --cli-connect-timeout 2 --cli-read-timeout 2 \
--endpoint-url=http://127.0.0.1:4567 \
s3api list-buckets预期命令以非零退出码结束,并报告无法连接 endpoint。这个反例稳定证明 SDK 使用了显式地址。更危险的错误是完全删除 --endpoint-url:AWS CLI 会按 region 选择真实 AWS endpoint,因此不要把它作为普通反向实验执行。项目启动器应在 local / test profile 下强制要求 endpoint,在生产 profile 下反过来拒绝 LocalStack 域名。
可在测试启动脚本里加入 fail-fast 守卫:
case "${LOCALSTACK_ENDPOINT:-}" in
http://localhost:4566|http://localhost.localstack.cloud:4566)
;;
*)
printf 'refuse to run: unsafe LocalStack endpoint: %s\n' \
"${LOCALSTACK_ENDPOINT:-<empty>}" >&2
exit 64
;;
esac
case "${AWS_ACCESS_KEY_ID:-}" in
AKIA*|ASIA*)
printf 'refuse to run with an AWS-style real access key\n' >&2
exit 65
;;
esac第二个反例针对 SQS 网络位置:在应用容器内对宿主机 Queue URL 执行请求,如果 URL 是 http://localhost:4566/...,预期连接本容器并失败。修复不是改 hosts 碰运气,而是让 SQS_ENDPOINT_STRATEGY、LOCALSTACK_HOST 与 Compose 网络统一,然后重新创建 queue,确认新 Queue URL 可从调用方解析。
SDK 接入必须把四个维度显式化
AWS SDK for Java v2 的本地 S3 客户端可以这样构造:
import java.net.URI;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.S3Client;
URI endpoint = URI.create(required("AWS_ENDPOINT_URL"));
S3Client s3 = S3Client.builder()
.endpointOverride(endpoint)
.region(Region.of(required("AWS_DEFAULT_REGION")))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build())
.build();SQS 客户端使用同一 endpointOverride、region 和假凭证,不需要 S3 的 path-style 配置。四个维度缺一不可:endpoint 决定网络目标,region 参与签名与资源身份,credentials provider 决定凭证来源,服务专属配置决定 S3/SQS 的 URL 行为。
生产构造器不要接受 AWS_ENDPOINT_URL 的任意覆盖,也不要把 test/test 设成全局默认。推荐拆成两个明确 profile:本地 profile 只有在 endpoint 命中允许列表且 access key 为假值时启动;AWS profile 使用工作负载身份、默认 endpoint 和目标 region,并拒绝 LocalStack 域名。启动日志可以打印 profile、region、endpoint、bucket 与 queue 逻辑名,但不能打印 secret、session token 或 LocalStack Auth Token。
测试夹具负责创建资源、注入唯一名称、等待结果和清理。业务代码只接收已经构造好的 S3/SQS client 与资源名,不在领域逻辑中判断“是不是 LocalStack”。这样真实 AWS smoke 可以复用同一业务调用,只替换 client factory 与资源配置。
CI 从空状态启动并留下失败证据
CI 使用专用 CI Auth Token,通过 secret 注入 LOCALSTACK_AUTH_TOKEN。开发者 token 不能用于 CI,官方 Auth Token 指南明确区分了两类 token。每个 job 使用独立容器和资源前缀,默认不开 persistence:
docker run --name localstack-ci --rm -d \
-p 127.0.0.1:4566:4566 \
-e LOCALSTACK_AUTH_TOKEN="$LOCALSTACK_AUTH_TOKEN" \
-e AWS_DEFAULT_REGION=us-east-1 \
-e PERSISTENCE=0 \
-e SQS_ENDPOINT_STRATEGY=path \
-v "$PWD/localstack/init:/etc/localstack/init:ro" \
localstack/localstack-pro:${LOCALSTACK_VERSION}
for attempt in $(seq 1 60); do
curl -fsS http://localhost:4566/_localstack/init/ready && break
if [ "$attempt" -eq 60 ]; then
docker logs localstack-ci
exit 1
fi
sleep 1
done随后运行 S3/SQS smoke 与项目集成测试。失败时先导出容器日志、init 状态、目标资源列表和测试报告,再停止容器。CI 不复用开发机 volume,不上传 token,不把含业务样本的完整 state 当作公开 artifact。并行 job 的 bucket、queue 和 object prefix 带 job id,避免同一 runner 上偶发冲突。
使用 Testcontainers 时,容器生命周期和测试绑定,动态映射 gateway 端口,并由测试代码读取实际 endpoint。它比 shell 管理更适合多 worker;Compose 则更适合开发机和跨语言仓库。共享 LocalStack 实例会引入状态、命名、许可、容量和故障域竞争,只有内部开发平台能提供租户隔离、资源回收、token 管理和审计时才值得采用。
persistence 改变的是状态模型与成本
默认实例是临时的,容器停止后状态丢失。设置 PERSISTENCE=1 并挂载 /var/lib/localstack 后,LocalStack 会保存并恢复服务状态。官方 Persistence提供以下策略:
| 字段 | 行为变化 | 代价 |
|---|---|---|
PERSISTENCE=1 | 重启后恢复资源与数据 | 旧状态污染、磁盘占用、版本兼容风险 |
SNAPSHOT_SAVE_STRATEGY=ON_REQUEST | 修改 API 后立即保存 | 服务保存期间加锁,写请求延迟上升 |
SNAPSHOT_SAVE_STRATEGY=SCHEDULED | 按周期保存已修改服务 | 崩溃时可能丢失最近窗口,默认折中方案 |
SNAPSHOT_SAVE_STRATEGY=ON_SHUTDOWN | 正常退出时统一保存 | 强杀或崩溃无法保证完整快照 |
SNAPSHOT_SAVE_STRATEGY=MANUAL | 只在调用 state save endpoint 时保存 | 调用方必须负责保存时机和失败处理 |
SNAPSHOT_LOAD_STRATEGY=ON_STARTUP | 启动时加载所有快照 | 启动更慢,但兼容错误更早暴露 |
SNAPSHOT_LOAD_STRATEGY=ON_REQUEST | 首次访问服务时延迟加载 | 首个请求承担恢复延迟 |
SNAPSHOT_LOAD_STRATEGY=MANUAL | 只在调用 state load endpoint 时恢复 | 加载前后的请求隔离由调用方保证 |
SCHEDULED 默认每 15 秒刷新一次,可通过 SNAPSHOT_FLUSH_INTERVAL 调整;把间隔缩短会增加磁盘写入和锁竞争,把间隔拉长则扩大崩溃时的数据丢失窗口。使用 MANUAL 时,可调用 POST /_localstack/state/<service>/save 与 load,或对全部服务调用 POST /_localstack/state/save 与 load,并逐行检查响应中的服务状态,不能只看 HTTP 连接成功。
持久化适合个人调试大型状态,不适合作为 CI 正确性的前提。升级前先导出需要的数据并在临时 volume 验证;LocalStack 会阻止已知不兼容状态加载,不要用 DISABLE_COMPATIBILITY_RULES=1 作为常规升级方案。状态格式、服务 provider 和目标版本不兼容时,可靠回滚是恢复原镜像与原 volume 的只读备份,或从 IaC、init hook 和合成数据重建。
清理自有资源:
aws --endpoint-url="$LOCALSTACK_ENDPOINT" s3 rm \
s3://te-order-dev-files/orders/ --recursive
aws --endpoint-url="$LOCALSTACK_ENDPOINT" sqs purge-queue \
--queue-url "$QUEUE_URL"
aws --endpoint-url="$LOCALSTACK_ENDPOINT" sqs delete-queue \
--queue-url "$QUEUE_URL"
aws --endpoint-url="$LOCALSTACK_ENDPOINT" s3api delete-bucket \
--bucket te-order-dev-filespurge-queue 只清空消息,不会删除队列;s3 rm --recursive 只删除当前对象,启用版本控制时还要清理历史 version 与 delete marker,bucket 才能删除。共享实例的清理脚本要先列出带本次 owner 前缀的资源,再逐个删除并复查为空,不能用一条全局 reset 掩盖归属错误。
独占开发环境需要彻底回到空状态时:
docker compose down -v
docker compose up -d localstackdown -v 会删除 Compose 管理的 LocalStack volume,只能对确认属于当前项目的独占环境执行。共享实例应按 owner 前缀删除资源,不允许全局 reset。
LocalStack 与真实 AWS 的差异要进入测试分层
LocalStack 能高效验证 SDK 调用、序列化、资源编排和服务间流程,但模拟器与真实 AWS 仍可能在 IAM condition、KMS key policy、S3 CORS 与预签名、SQS 时序与配额、区域可用性、事件投递、Lambda runtime、VPC/DNS/TLS、托管服务扩缩容和错误节流上不同。每个服务页都提供 API coverage 或限制说明,例如 SQS 当前限制会直接影响特定属性变更测试。
测试分层应形成闭环:
单元测试替换业务接口,快速覆盖纯逻辑。LocalStack 集成测试从空状态创建资源,覆盖 SDK、IaC、S3/SQS 数据流和失败恢复。真实开发 AWS 账号运行最小契约 smoke,覆盖 IAM、KMS、region、网络、配额与事件语义。
预发布环境验证与生产相同的工作负载身份、监控、扩缩容和成本保护。
真实云 smoke 使用短期身份和最小权限,资源带 TTL 与 owner 标签,测试后按清单删除。失败时保留 request id、CloudTrail、服务事件和 LocalStack 对照结果。不能因为 LocalStack 通过就跳过真实云,也不能把真实账号凭证注入本地模拟器来“提高真实性”。
凭证、敏感数据与网络保护
LOCALSTACK_AUTH_TOKEN 是许可与服务激活凭证,不进入 Git、镜像层、命令回显或普通日志;泄漏后立即旋转。AWS 假凭证与 Auth Token 不是同一个东西:前者满足 AWS 签名链,后者激活 LocalStack 能力。Docker 环境变量仍可能被拥有 daemon 权限的用户通过容器 inspect 读取,因此共享 runner 必须隔离项目和权限;CI secret 应限制到受信分支和 runner,fork PR 不能直接获得 token。
前面的 Compose 显式把 gateway 发布到宿主机 127.0.0.1,这才阻止同网段机器直接访问;不能把容器内部监听地址误当成宿主机暴露边界。若要给局域网或共享 runner 使用,必须增加防火墙、反向代理认证、TLS 与租户隔离,而不是简单改成 0.0.0.0:4566。Docker socket 只在所需服务启用,并使用专用 runner;init hook 与挂载目录视为可执行代码审查。
S3 对象、SQS body、Lambda 环境变量、Cloud Pods 和持久化 state 都可能包含个人信息、访问令牌或生产样本。使用合成数据,日志做字段级脱敏,volume 与 artifact 设保留期。开发者离职或项目结束时,同时撤销 LocalStack 用户许可、轮换 token、删除共享 state 与清理 CI secret。
容量、费用与长期治理
本地模拟消除了大量按请求的 AWS 开发费用,却引入 LocalStack 订阅、CI 分钟、runner CPU/内存、镜像存储、持久化磁盘和维护时间。Cloud Sandbox 分钟、Cloud Pod 存储及共享基础设施的授权方式与计划相关,预算时不能只比较 AWS 账单。
容量基线至少记录:每个 job 的启动时间、峰值内存、S3 数据量、SQS 消息数、并行容器数、volume 增长、失败重试次数和真实云 smoke 消耗。趋势判据比固定数字可靠:空状态 CI 的耗时不随历史轮次增长,job 结束后容器与资源回到基线,volume 不无限膨胀,token 使用能追溯到用户或流水线,真实云残留资源为零。
升级流程先读取目标版本 release notes 和服务 coverage,在临时环境执行 init hook、S3/SQS 正反实验、SDK 集成测试与真实云 smoke;随后更新锁定镜像。若 persistence 被启用,升级演练还要验证状态兼容和回滚副本。计划或许可变更时重新核对开发者席位、CI token、共享平台和自动化用量,避免把个人 Hobby 环境演变成无人负责的商业共享服务。
LocalStack 的最佳位置是“快速而可重建的云 API 集成层”:资源由代码创建,endpoint 与假凭证被强制约束,CI 从空状态开始,真实 AWS 保留最小复核,状态、许可和成本都有 owner。做到这些,S3 成功而 SQS 失败时,团队能沿 gateway、Queue URL、SDK 配置、init 状态和真实云差异逐层取证,而不是反复重启一个看似万能的本地云。
