LocalStack
订单服务在本地把文件写进 S3 后返回成功,异步消费者却一直收不到消息。排查时才发现,S3 客户端显式连接了 4566 Gateway,SQS 客户端却沿用 SDK 默认 endpoint;另一组测试拿到含 localhost 的 Queue URL 后把它交给 Compose 内的应用容器,容器把 localhost 解释成了自己;还有一次 CI 复用了旧 volume,初始化脚本查询到历史队列后误以为本轮环境已经就绪。这三种失败分别来自请求入口、网络视角和状态生命周期,反复重启同一个容器不会自动修好。
LocalStack 在本机或 CI 中提供一组与 AWS API 兼容的服务端点,使应用可以在不访问真实云账户的情况下完成资源创建、SDK 调用、事件链路和基础设施代码测试。它解决的是开发反馈速度、测试隔离和云资源成本问题,不是把一台 Docker 主机变成生产 AWS,也不能证明真实 IAM、网络、配额和托管基础设施行为完全正确。
一、是什么
LocalStack 究竟替代了什么
LocalStack 的准确定位
LocalStack 是 AWS 云服务模拟平台。应用仍然使用 AWS CLI、AWS SDK、Terraform、CloudFormation 或 CDK,只是把 endpoint 指向 LocalStack。请求进入统一 Gateway 后,LocalStack 根据 Host、路径、签名、服务协议、账户和区域选择对应 provider,并把资源状态保存在当前实例中。
“兼容 AWS API”不等于“内部实现等同 AWS”。LocalStack 可以模拟 API 形状、常见状态机、服务间事件和错误响应,但不会复制 AWS 的物理控制面、多可用区基础设施、全球网络、真实容量、全部 IAM 条件、托管升级和所有边缘行为。正确用途是开发与集成测试,最终契约仍要在隔离 AWS 账户验证。
LocalStack 默认是临时测试环境。容器删除后状态消失,测试可以从干净环境重新开始。启用持久化后可以保留本地状态,但它仍不是生产备份系统,也不提供生产级高可用、灾难恢复或数据耐久承诺。
官方资源、版本线、发布与许可
| 资源 | 地址 | 用途 |
|---|---|---|
| 官网 | localstack.cloud | 产品入口、计划与账户 |
| 文档 | LocalStack for AWS Docs | 安装、服务、SDK、网络和状态管理 |
| 服务覆盖 | Local AWS Services | 查询服务和 API 覆盖范围 |
| 安装 | Installation | Docker、Compose、CLI 与 Kubernetes |
| 镜像 | localstack/localstack | 版本标签、架构与镜像摘要 |
| 发布变化 | Changelog | 功能、修复、弃用和升级影响 |
| 配置 | Configuration | 环境变量和默认值 |
| 历史源码 | localstack/localstack | 已归档的历史公开仓库 |
| 许可计划 | LocalStack Plans | Hobby、Base、Ultimate、Enterprise 等能力边界 |
当前示例锁定 CalVer 版本标识 2026.08.0。从首个 2026.03.x CalVer 版本标识开始,正式镜像采用 YYYY.MM.patch:精确 patch 标签不移动,YYYY.MM 会跟随当月补丁,stable 与 latest 都只跟随正式 tagged release,dev 才跟随尚未发布的 main 提交。团队开发和 CI 应锁定精确 patch;供应链要求更高时再锁定镜像 digest,并分别记录 linux/amd64 与 linux/arm64 的实际 image ID。
当前 LocalStack for AWS 使用统一镜像和 Auth Token 激活账户权益。是否能够使用某个服务,不能只看服务覆盖页,还要同时核对订阅计划、具体服务页、API 覆盖和当前镜像版本。历史公开仓库、镜像分发、平台服务与商业计划的许可边界不同,企业使用前应按官方许可页和采购合同确认,不应仅根据旧版本的 Apache License 结论推断当前全部产品权益。
一次请求如何进入 provider 并留下状态
总体架构
| 层次 | 核心职责 | 需要关注的边界 |
|---|---|---|
| 客户端层 | AWS CLI、SDK、Terraform、CDK、CloudFormation | endpoint、region、凭证、重试和寻址方式 |
| Gateway | 在 4566 接收 AWS API 请求并识别服务 | Host、路径、协议、TLS、DNS 与代理 |
| 协议层 | 解析 REST、Query、JSON、XML 等 AWS 协议 | API 覆盖和错误响应可能与 AWS 存在差异 |
| Provider | 执行 S3、SQS、Lambda、DynamoDB 等服务逻辑 | 服务实现与计划决定能力深度 |
| 状态层 | 按账户、区域和服务保存资源状态 | 默认临时;持久化存在版本兼容边界 |
| 计算层 | 为 Lambda、ECS、EC2、RDS 等启动额外运行环境 | 常依赖 Docker socket、镜像、网络和宿主资源 |
| 内部接口 | 健康、诊断、状态管理和 LocalStack 扩展能力 | /_localstack 与 /_aws 不是 AWS 正式 API |
典型请求链是:应用构造 AWS 请求,endpoint 把请求送到 4566,Gateway 从 Host、签名和协议识别服务,provider 在指定账户与区域中读取或修改状态,必要时启动子容器,最后返回 AWS 风格响应。S3 的虚拟主机寻址还会把 bucket 放入 Host;Lambda 则可能在另一个容器中运行并回调 LocalStack 服务。
Gateway、DNS、endpoint 与 TLS
主入口是 http://localhost.localstack.cloud:4566。该域名及其子域通常解析到本机,适合 S3 虚拟主机寻址;无法解析时可退回 http://127.0.0.1:4566,并让 S3 客户端启用 path-style。
LocalStack 内置 DNS 可以把 AWS 域名解析到模拟环境,用于不能显式修改 endpoint 的工作负载。透明注入便利但风险更高:DNS 或证书配置错误可能把请求发往真实 AWS,或者让本应访问真实服务的请求被重定向。普通项目优先显式设置 endpoint,只有 CDK 自定义资源、Lambda 内部调用等确实需要时才使用透明注入。
HTTPS 可以使用 localhost.localstack.cloud 证书,但官方证书只覆盖一部分区域域名。证书不匹配时不要全局关闭 TLS 验证,应先核对 hostname、区域、代理和证书覆盖;本地 HTTP 已满足大多数隔离开发场景。
账户、区域、凭证和状态隔离
AWS 资源通常由账户和区域共同命名。LocalStack 也会根据 access key 和 region 区分状态,因此“资源明明创建了但查询不到”常常不是丢失,而是客户端切换了凭证、账户映射或区域。
本地示例使用明显的假凭证 test/test,禁止把生产 access key 放入 LocalStack。默认账户经常显示为 000000000000,但测试不应把这个值散落在业务代码里,应从 STS 响应或测试配置取得。启用更严格的账号和 IAM 行为后,账户映射可能变化,必须以运行时结果为准。
服务模拟的真实边界
服务覆盖表说明“存在实现”,不代表每个 API、错误码、时序、IAM 条件和配额都与 AWS 等价。最容易出现假阳性的部分是权限、KMS、跨账户、网络边界、最终一致性、服务配额、节流、托管故障、证书、DNS、事件重试和区域差异。
LocalStack 成功可以证明应用序列化请求、解析响应、连接多个 AWS 服务和执行常见资源生命周期;它不能单独证明生产权限最小化、VPC 路由正确、KMS policy 可用、AWS quota 足够或灾难恢复有效。架构设计必须明确哪些测试在本地完成,哪些必须进入隔离 AWS 账户。
初始化、状态和生命周期
LocalStack 支持 BOOT、START、READY、SHUTDOWN 初始化阶段,脚本放在 /etc/localstack/init/<stage>.d。创建 AWS 资源通常放在 ready.d,因为此时 Gateway 已经可以接收请求。初始化脚本必须可重复执行,并让错误以非零退出;输出一行“成功”不能替代资源查询验证。
默认状态随实例销毁。PERSISTENCE=1 会把服务状态保存到 /var/lib/localstack/state,还可以通过内部 state endpoint 手动保存或加载。状态格式会随服务演进,旧状态可能被兼容规则拒绝;尤其不能把跨大版本复制 volume 当成可靠升级方案。
二、为什么
先决定哪一层使用 LocalStack
适合使用 LocalStack 的场景
| 场景 | LocalStack 的价值 | 仍需补充的验证 |
|---|---|---|
| SDK 集成 | 不访问真实云即可验证请求、响应和异常处理 | 在 AWS 复核凭证链、权限和区域差异 |
| 多服务事件链 | 本地联调 S3、SQS、Lambda、SNS、EventBridge | 在 AWS 复核投递重试、并发、配额和失败目的地 |
| IaC 测试 | 验证 Terraform、CloudFormation、CDK 的资源依赖 | 在 AWS 执行 plan、创建与销毁契约 |
| CI 集成测试 | 每个 job 启动独立实例,反馈快且成本可控 | 使用隔离 AWS 账户保留少量真实云门禁 |
| 离线开发 | 网络不稳定时仍可开发常用流程 | 首次镜像和运行时依赖仍可能需要网络 |
| 故障注入 | 主动停止容器、制造 endpoint 和状态错误 | AWS 托管故障不能由本地容器完全模拟 |
最有价值的用法不是“把所有 AWS 都搬到本机”,而是选出应用真正依赖的服务契约,让开发者在提交前快速发现 endpoint、序列化、事件格式、资源依赖和清理问题。
不应该只依赖 LocalStack 的场景
生产容量评估、跨可用区容灾、真实 IAM/KMS 审批、VPC 网络、WAF、CloudFront 边缘行为、服务配额、账单、托管升级和合规审计不能只靠 LocalStack。依赖 undocumented AWS 行为、复杂跨账户授权或新发布 API 时,也应优先建立真实 AWS 契约测试。
LocalStack 不是生产对象存储,不应该承载业务正式数据;不是共享开发数据库,不应该让所有团队长期共用一个脏实例;也不是 AWS 私有化替代品,不能把单容器部署包装成生产云平台。
测试分层
| 层次 | 运行位置 | 主要目标 | 失败说明什么 |
|---|---|---|---|
| 单元测试 | 进程内 fake 或 mock | 业务分支、异常和纯函数 | 代码逻辑错误 |
| LocalStack 集成测试 | 本机或 CI 容器 | SDK、IaC、序列化、资源生命周期、事件链 | 应用与 AWS 风格 API 集成错误 |
| AWS 契约测试 | 隔离 AWS 账户 | IAM、KMS、网络、配额、时序和真实服务差异 | 本地模拟无法覆盖的云契约错误 |
| 预生产验证 | 接近生产的 AWS 环境 | 部署、监控、容量、回滚和运维流程 | 发布链路或架构风险 |
同一份核心契约应尽量支持 LocalStack 和 AWS 两个目标,但 endpoint 与凭证必须由测试环境明确注入。AWS 任务不允许继承 LocalStack endpoint,本地任务不允许使用生产凭证;这个隔离比编写复杂自动化证明更重要。
再用服务契约和成本选择工具
与其他工具的选型边界
| 工具 | 最适合 | 主要限制 |
|---|---|---|
| LocalStack | 多 AWS 服务、SDK、IaC 和事件链集成 | 不是所有服务和行为都与 AWS 等价 |
| Moto | Python 测试中的轻量 AWS API mock | 主要服务 Python 测试,跨进程和多语言能力较弱 |
| MinIO | S3 兼容对象存储与对象 API 开发 | 不是 AWS 多服务模拟器,S3 行为也不完全等价 |
| WireMock | 明确 HTTP 契约和异常响应 | 需要自行维护 AWS 请求与响应样本 |
| Testcontainers | 把依赖容器生命周期绑定到测试 | 它负责容器,不自动解决 AWS 模拟准确性 |
| 隔离 AWS 账户 | 最真实的 AWS 行为 | 速度、成本、清理、安全和配额治理更复杂 |
只测试 Python 函数时 Moto 更快;只需要对象 API 时 MinIO 或 S3Mock 更简单;需要 S3 触发 SQS/Lambda、Terraform 或多语言 SDK 时 LocalStack 更合适;影响权限、网络、KMS 和生产发布的契约必须回到 AWS。
架构决策应回答的问题
选择 LocalStack 前应明确服务清单、必须覆盖的 API、计划权益、允许的行为偏差、测试数据生命周期、CI 并发量、Docker socket 风险、真实 AWS 契约频率和失败后的责任边界。没有这些答案时,团队容易把“本地接口返回成功”误当成“生产设计正确”。
推荐的通过标准是:本地集成测试验证资源创建、业务调用、异常和清理;真实 AWS 契约验证关键权限、网络、加密、事件时序和删除;预生产验证监控、容量、升级和回滚。三层证据互补,不能互相替代。
三、怎么做
从空目录启动一套可重建环境
准备 Linux 环境
开发机至少需要 4 核 CPU、8 GiB 内存和 20 GiB 可用磁盘;Lambda、OpenSearch、RDS 等服务会拉取额外镜像或启动子容器,需要更多资源。先检查宿主:
uname -m
cat /etc/os-release
free -h
df -hx86_64 或 aarch64 应与目标镜像架构一致;可用内存和磁盘不足时先清理。先安装基础工具:
sudo apt-get update
sudo apt-get install -y ca-certificates curl unzip jq zipUbuntu 或 Debian 使用 Docker 官方 apt 仓库。/etc/os-release 中的 ID 必须是 ubuntu 或 debian,否则停止并选择对应官方安装页:
source /etc/os-release
test "$ID" = ubuntu -o "$ID" = debian || exit 1
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL "https://download.docker.com/linux/$ID/gpg" \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
suite="${UBUNTU_CODENAME:-$VERSION_CODENAME}"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $suite stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now dockerRHEL 使用 Docker 官方 RHEL 仓库;Rocky Linux 和 AlmaLinux 使用兼容的 CentOS 仓库前,应先得到组织的软件源许可:
sudo dnf install -y dnf-plugins-core curl unzip jq zip
source /etc/os-release
test "$ID" = rhel -o "$ID" = rocky -o "$ID" = almalinux || exit 1
repo_family=rhel
test "$ID" = rocky -o "$ID" = almalinux && repo_family=centos
sudo dnf config-manager --add-repo \
"https://download.docker.com/linux/$repo_family/docker-ce.repo"
sudo dnf install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker验证 daemon、Compose 和最小容器:
sudo docker version
sudo docker compose version
sudo docker run --rm hello-world
curl --version
jq --version三条 Docker 命令都必须为 0,hello-world 应输出成功说明。后文默认当前用户已按组织规范获得 Docker 权限;否则给 Docker 命令保留 sudo。不要把 socket 权限开放给所有用户;把账号加入 docker 组接近授予宿主 root 权限,必须只用于受信开发账号并重新登录后用 docker info 验证。
安装 AWS CLI v2 时按宿主架构选择官方包:
arch="$(uname -m)"
test "$arch" = x86_64 && package=awscli-exe-linux-x86_64.zip
test "$arch" = aarch64 && package=awscli-exe-linux-aarch64.zip
test -n "${package:-}" || exit 1
curl -fL "https://awscli.amazonaws.com/$package" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install --update
aws --versionaws --version 应返回 v2。package 为空说明当前架构未被上述命令覆盖,应停止安装并查官方安装页。
创建项目目录和保护 Auth Token
mkdir -p localstack-lab/init/ready.d
cd localstack-lab
printf '.env.local\nvolume/\nartifacts/\n' > .gitignore从 LocalStack Web Application 创建 Developer Token 或 CI Auth Token。开发机用交互输入写入本地文件:
read -rsp 'LocalStack Auth Token: ' LOCALSTACK_AUTH_TOKEN
printf '\nLOCALSTACK_AUTH_TOKEN=%s\n' "$LOCALSTACK_AUTH_TOKEN" \
> .env.local
chmod 600 .env.local
unset LOCALSTACK_AUTH_TOKEN.env.local 只能由当前用户读取,且必须被 Git 忽略。CI 使用密钥管理器注入,不把 token 写入镜像、日志或仓库。Token 泄露后立即在平台重置,旧 token 不能继续使用。
Docker Compose 部署
创建 compose.yaml:
name: ${COMPOSE_PROJECT_NAME:-localstack-lab}
services:
localstack:
image: localstack/localstack:2026.08.0
ports:
- "127.0.0.1:4566:4566"
- "127.0.0.1:4510-4559:4510-4559"
environment:
LOCALSTACK_AUTH_TOKEN: ${LOCALSTACK_AUTH_TOKEN:?}
DEBUG: ${DEBUG:-0}
PERSISTENCE: ${PERSISTENCE:-0}
SNAPSHOT_SAVE_STRATEGY: ${SNAPSHOT_SAVE_STRATEGY:-SCHEDULED}
SNAPSHOT_LOAD_STRATEGY: ${SNAPSHOT_LOAD_STRATEGY:-ON_REQUEST}
SNAPSHOT_FLUSH_INTERVAL: ${SNAPSHOT_FLUSH_INTERVAL:-15}
SQS_ENDPOINT_STRATEGY: ${SQS_ENDPOINT_STRATEGY:-standard}
SERVICES: s3,sqs,lambda,dynamodb,sns,events,iam,sts,logs
AWS_DEFAULT_REGION: us-east-1
volumes:
- ./volume:/var/lib/localstack
- ./init/ready.d:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:4566/_localstack/health"]
interval: 5s
timeout: 3s
retries: 30
start_period: 20s4566 是 Gateway。4510-4559 只在返回独立服务端口的场景需要;不需要时可以删除映射。Docker socket 用于 Lambda 等计算服务启动子容器,它同时带来宿主控制风险,因此只在隔离开发机和受控 runner 使用,不把该实例暴露到不受信网络。
先渲染配置,避免变量缺失后直接启动:
docker compose --env-file .env.local config --quiet
docker compose --env-file .env.local pull
docker compose --env-file .env.local up -d检查容器、端口和健康状态:
docker compose ps
ss -lntp | grep 4566
curl -fsS http://localhost.localstack.cloud:4566/_localstack/health | jq
curl -fsS http://localhost.localstack.cloud:4566/_localstack/init/ready | jq
curl -fsS http://localhost.localstack.cloud:4566/_localstack/info | jq容器应为 healthy,端口只绑定 127.0.0.1:4566。三个接口不能互相替代:health 证明 Gateway 和服务状态可读,init endpoint 证明 ready.d 已经完成,info 中的 is_license_activated 证明当前权益已经激活。测试只能在初始化完成且许可激活后开始。再确认响应确实来自 LocalStack 并记录版本:
curl -fsSI http://localhost.localstack.cloud:4566/_localstack/health \
| grep -i '^x-localstack:'
docker compose images
docker image inspect localstack/localstack:2026.08.0 \
--format '{{.Id}} {{.Os}}/{{.Architecture}}'x-localstack 响应头应包含运行版本。镜像 ID、OS 和架构应写入 CI 测试日志,用于定位不同 runner 的差异。
配置 AWS CLI 的本地身份
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 AWS_ENDPOINT_URL=http://localhost.localstack.cloud:4566
export AWS_PAGER=''验证 endpoint、账户和区域:
aws sts get-caller-identity --endpoint-url "$AWS_ENDPOINT_URL"
aws configure list输出应包含本地账户和 us-east-1。AWS_EC2_METADATA_DISABLED=true 会阻止凭证链在假凭证缺失时继续探测实例元数据;开发 shell 也不应保留 AKIA、ASIA 形态的真实 access key。若命令访问了公网 AWS、要求 MFA 或返回真实组织账号,立即停止并检查 AWS_ENDPOINT_URL、shell 环境和 profile,不能继续创建资源。
使用幂等初始化钩子
创建 init/ready.d/10-bootstrap.sh:
#!/bin/sh
set -eu
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
awslocal s3api head-bucket --bucket ls-app-dev 2>/dev/null || \
awslocal s3api create-bucket --bucket ls-app-dev
awslocal sqs get-queue-url --queue-name ls-events-dev >/dev/null 2>&1 || \
awslocal sqs create-queue --queue-name ls-events-dev >/dev/null赋予执行权限并重建容器:
chmod 0755 init/ready.d/10-bootstrap.sh
docker compose --env-file .env.local up -d --force-recreate
docker compose logs localstack | tail -n 100日志不应包含 hook 失败。资源查询才是最终验证:
aws s3api head-bucket \
--bucket ls-app-dev --endpoint-url "$AWS_ENDPOINT_URL"
aws sqs get-queue-url \
--queue-name ls-events-dev --endpoint-url "$AWS_ENDPOINT_URL"重复重建后两项仍应成功且资源没有重复。脚本只做必要初始化,不把所有测试、清理和诊断塞进一个启动脚本。
用同一条订单链跑通四种 AWS 服务
S3 完整操作
上传测试对象:
printf '{"orderId":"o-1001","status":"CREATED"}\n' > order.json
aws s3 cp order.json s3://ls-app-dev/inbox/order.json \
--endpoint-url "$AWS_ENDPOINT_URL"读取元数据和内容:
aws s3api head-object \
--bucket ls-app-dev --key inbox/order.json \
--endpoint-url "$AWS_ENDPOINT_URL"
aws s3 cp s3://ls-app-dev/inbox/order.json - \
--endpoint-url "$AWS_ENDPOINT_URL" | jq -e '.orderId == "o-1001"'head-object 应包含正确的 ContentLength 和 ETag,jq 退出码应为 0。使用 localhost:4566 时,SDK 应启用 path-style;使用 s3.localhost.localstack.cloud:4566 时可以测试虚拟主机寻址。两种寻址不要在同一套客户端配置中混用。
生成预签名 URL 后必须真正下载验证:
url="$(aws s3 presign s3://ls-app-dev/inbox/order.json \
--expires-in 300 --endpoint-url "$AWS_ENDPOINT_URL")"
curl -fsS "$url" | jq -e '.status == "CREATED"'浏览器场景还要验证 CORS、hostname 和 TLS;CLI 下载成功不能证明浏览器跨域一定成功。
SQS 完整操作
取得队列 URL 并发送消息:
QUEUE_URL="$(aws sqs get-queue-url \
--queue-name ls-events-dev \
--endpoint-url "$AWS_ENDPOINT_URL" \
--query QueueUrl --output text)"
aws sqs send-message \
--queue-url "$QUEUE_URL" \
--message-body '{"orderId":"o-1001"}' \
--endpoint-url "$AWS_ENDPOINT_URL"长轮询接收并确认消息:
message="$(aws sqs receive-message \
--queue-url "$QUEUE_URL" \
--wait-time-seconds 10 --max-number-of-messages 1 \
--endpoint-url "$AWS_ENDPOINT_URL")"
printf '%s\n' "$message" | jq -e '.Messages | length == 1'
handle="$(printf '%s\n' "$message" | jq -r '.Messages[0].ReceiptHandle')"
aws sqs delete-message \
--queue-url "$QUEUE_URL" --receipt-handle "$handle" \
--endpoint-url "$AWS_ENDPOINT_URL"删除后再次接收应没有 Messages。业务消费者必须处理重复投递和 visibility timeout,不能因为本地测试只收到一次就省略幂等设计。
Queue URL 不是普通展示字段,它会成为后续 receive、delete 和 event source mapping 的目标地址。宿主机直接调用时,SQS_ENDPOINT_STRATEGY=standard 会返回包含 region 与 account 的 AWS 风格 URL;应用在另一个 Compose 容器中运行时,需要让 LOCALSTACK_HOST 指向该容器能够解析的服务名,并按调用方网络选择 path 策略。修改策略后要重新创建队列,再从真实应用容器执行 GetQueueUrl 和一次收发删除闭环;不要把旧 URL 缓存在配置中,也不要在业务代码里拆字符串重拼 hostname。
容器调用方对应的最小 Compose 配置是:
environment:
LOCALSTACK_HOST: localstack:4566
SQS_ENDPOINT_STRATEGY: path重建 LocalStack 和队列后先观察服务端实际返回值:
docker compose exec localstack awslocal sqs get-queue-url \
--queue-name ls-events-dev --output jsonQueueUrl 应以 http://localstack:4566/queue/ 开头;随后必须在应用容器中使用这个 URL 完成发送、接收和删除。若仍返回 localhost,说明环境变量没有进入当前容器,或查询到的是重建前的旧队列。
Lambda 完整操作
创建最小 Python 函数 lambda_function.py:
import json
def handler(event, context):
records = event.get("Records", [])
return {
"statusCode": 200,
"body": json.dumps({
"requestId": context.aws_request_id,
"recordCount": len(records),
}),
}创建本地 IAM role。信任策略保存为 trust.json:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}执行创建:
zip -q function.zip lambda_function.py
aws iam create-role \
--role-name ls-lambda-role \
--assume-role-policy-document file://trust.json \
--endpoint-url "$AWS_ENDPOINT_URL"
role_arn="$(aws iam get-role \
--role-name ls-lambda-role \
--endpoint-url "$AWS_ENDPOINT_URL" \
--query Role.Arn --output text)"创建函数并等待 Active:
aws lambda create-function \
--function-name ls-order-handler \
--runtime python3.13 \
--handler lambda_function.handler \
--role "$role_arn" \
--zip-file fileb://function.zip \
--endpoint-url "$AWS_ENDPOINT_URL"
aws lambda wait function-active-v2 \
--function-name ls-order-handler \
--endpoint-url "$AWS_ENDPOINT_URL"调用并验证业务响应:
aws lambda invoke \
--function-name ls-order-handler \
--payload '{"Records":[{"eventSource":"local-test"}]}' \
--cli-binary-format raw-in-base64-out \
--endpoint-url "$AWS_ENDPOINT_URL" response.json
jq -e '.statusCode == 200' response.json
jq -r '.body' response.json | jq -e '.recordCount == 1'函数卡在 Pending 或 Failed 时先检查 Docker socket、宿主架构、运行时镜像、代理和子容器日志,不要只提高 waiter 次数。LocalStack 的 Lambda 权限模拟不能替代 AWS 中 execution role 的最小权限验证。
DynamoDB 完整操作
aws dynamodb create-table \
--table-name ls-orders \
--attribute-definitions AttributeName=orderId,AttributeType=S \
--key-schema AttributeName=orderId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--endpoint-url "$AWS_ENDPOINT_URL"
aws dynamodb wait table-exists \
--table-name ls-orders --endpoint-url "$AWS_ENDPOINT_URL"写入和条件更新:
aws dynamodb put-item \
--table-name ls-orders \
--item '{"orderId":{"S":"o-1001"},"status":{"S":"CREATED"}}' \
--condition-expression 'attribute_not_exists(orderId)' \
--endpoint-url "$AWS_ENDPOINT_URL"
aws dynamodb update-item \
--table-name ls-orders \
--key '{"orderId":{"S":"o-1001"}}' \
--update-expression 'SET #s = :paid' \
--condition-expression '#s = :created' \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values \
'{":created":{"S":"CREATED"},":paid":{"S":"PAID"}}' \
--endpoint-url "$AWS_ENDPOINT_URL"验证最终状态:
aws dynamodb get-item \
--table-name ls-orders \
--key '{"orderId":{"S":"o-1001"}}' \
--consistent-read \
--endpoint-url "$AWS_ENDPOINT_URL" \
| jq -e '.Item.status.S == "PAID"'条件表达式应在 LocalStack 和 AWS 契约测试中都执行,避免本地流程只验证“能写入”而没有验证并发保护。
把本地契约接入应用与自动化测试
Python 项目接入
Ubuntu 或 Debian 安装 Python 运行环境:
sudo apt-get install -y python3 python3-venv python3-pip
python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 10))'
python3 -m venv --help >/dev/null
python3 -m pip --versionRHEL 系安装 python3 和 python3-pip 后执行同一版本门。Boto3 1.43.82 要求 Python 3.10 或更高;任一检查失败时停止,不进入依赖下载。
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install 'boto3==1.43.82'
python -m pip freeze > requirements.txt创建 app.py:
import json
import os
import sys
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
endpoint = os.environ["AWS_ENDPOINT_URL"]
config = Config(
connect_timeout=2,
read_timeout=5,
retries={"max_attempts": 3, "mode": "standard"},
s3={"addressing_style": "path"},
)
s3 = boto3.client(
"s3",
endpoint_url=endpoint,
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", "test"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", "test"),
config=config,
)
try:
body = json.dumps({"source": "python", "ok": True}).encode()
s3.put_object(Bucket="ls-app-dev", Key="sdk/python.json", Body=body)
response = s3.get_object(Bucket="ls-app-dev", Key="sdk/python.json")
payload = json.loads(response["Body"].read())
if payload != {"source": "python", "ok": True}:
raise RuntimeError(f"unexpected payload: {payload!r}")
print(payload)
except (BotoCoreError, ClientError, RuntimeError) as error:
print(f"localstack request failed: {error}", file=sys.stderr)
raise SystemExit(1) from error
finally:
s3.close()python app.py输出应为 {'source': 'python', 'ok': True}。生产配置不能默认回退到本地 endpoint;建议由明确的测试 profile 才允许注入 LocalStack 地址。
Node.js 项目接入
使用 Node.js 官方支持的 20 或更高版本。安装方式随发行版变化,先按 Node.js Download 安装组织批准的 LTS,再执行版本门:
node --version
npm --version
node -e 'const major=+process.versions.node.split(".")[0]; process.exit(major < 20)'任一命令失败时停止,不执行 npm install。
mkdir localstack-node && cd localstack-node
npm init -y
npm install @aws-sdk/client-s3@3.1116.0 @smithy/node-http-handler
npm pkg set type=module创建 app.mjs:
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { NodeHttpHandler } from '@smithy/node-http-handler';
const endpoint = process.env.AWS_ENDPOINT_URL;
if (!endpoint) throw new Error('AWS_ENDPOINT_URL is required');
const client = new S3Client({
endpoint,
region: process.env.AWS_DEFAULT_REGION ?? 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
},
forcePathStyle: true,
maxAttempts: 3,
requestHandler: new NodeHttpHandler({
connectionTimeout: 2_000,
requestTimeout: 5_000,
}),
});
try {
const expected = JSON.stringify({ source: 'node', ok: true });
await client.send(new PutObjectCommand({
Bucket: 'ls-app-dev', Key: 'sdk/node.json', Body: expected,
ContentType: 'application/json',
}));
const result = await client.send(new GetObjectCommand({
Bucket: 'ls-app-dev', Key: 'sdk/node.json',
}));
const actual = await result.Body.transformToString();
if (actual !== expected) throw new Error(`unexpected payload: ${actual}`);
console.log(actual);
} finally {
client.destroy();
}node app.mjs输出 JSON 中 source 应为 node。连接超时、请求超时和重试必须有限;写操作是否允许重试还要结合幂等 key 或业务 token 设计。
Java 项目接入
Ubuntu 或 Debian 安装 JDK 17 和 Maven:
sudo apt-get install -y openjdk-17-jdk maven
java -version
javac -version
mvn -versionRHEL 系使用 sudo dnf install -y java-17-openjdk-devel maven,再执行同一版本门。javac 主版本不是 17 时停止。先创建源码目录:
mkdir -p src/main/java/example创建 pom.xml,使用 AWS SDK BOM 统一版本:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>localstack-java</artifactId>
<version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release></properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.54.3</version>
<type>pom</type><scope>import</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>2.0.5</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>s3</artifactId></dependency>
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>url-connection-client</artifactId></dependency>
<dependency><groupId>org.testcontainers</groupId><artifactId>testcontainers-localstack</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.testcontainers</groupId><artifactId>testcontainers-junit-jupiter</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.12.2</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId><version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId><version>3.5.3</version>
</plugin>
</plugins></build>
</project>创建 src/main/java/example/App.java:
package example;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
public final class App {
public static void main(String[] args) {
String endpoint = System.getenv("AWS_ENDPOINT_URL");
if (endpoint == null || endpoint.isBlank()) {
throw new IllegalStateException("AWS_ENDPOINT_URL is required");
}
var timeout = ClientOverrideConfiguration.builder()
.apiCallAttemptTimeout(Duration.ofSeconds(5))
.apiCallTimeout(Duration.ofSeconds(10)).build();
try (S3Client s3 = S3Client.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true).build())
.overrideConfiguration(timeout).build()) {
String expected = "{\"source\":\"java\",\"ok\":true}";
s3.putObject(PutObjectRequest.builder().bucket("ls-app-dev")
.key("sdk/java.json").contentType("application/json").build(),
RequestBody.fromString(expected));
var response = s3.getObjectAsBytes(GetObjectRequest.builder()
.bucket("ls-app-dev").key("sdk/java.json").build());
String actual = response.asString(StandardCharsets.UTF_8);
if (!expected.equals(actual)) {
throw new IllegalStateException("unexpected payload: " + actual);
}
System.out.println(actual);
}
}
}mvn -q package
mvn -q exec:java -Dexec.mainClass=example.App输出应包含 "source":"java"。真实项目应把本地和 AWS client factory 分开:本地 factory 必须显式 endpoint 和假凭证,AWS factory 不允许 endpoint override,并使用正式凭证链。
使用 Testcontainers 隔离测试实例
前一节 POM 已包含 Testcontainers 2.0.5、JUnit Jupiter 和 Surefire。创建 src/test/java/example/LocalStackTest.java:
package example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.localstack.LocalStackContainer;
import org.testcontainers.utility.DockerImageName;
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.S3Client;
@Testcontainers
final class LocalStackTest {
private static final String TOKEN = System.getenv("LOCALSTACK_AUTH_TOKEN");
@Container
static final LocalStackContainer LOCALSTACK =
new LocalStackContainer(DockerImageName.parse(
"localstack/localstack:2026.08.0"))
.withEnv("LOCALSTACK_AUTH_TOKEN", requireToken());
private static String requireToken() {
if (TOKEN == null || TOKEN.isBlank()) {
throw new IllegalStateException("LOCALSTACK_AUTH_TOKEN is required");
}
return TOKEN;
}
@Test
void createsBucket() {
var credentials = StaticCredentialsProvider.create(
AwsBasicCredentials.create(
LOCALSTACK.getAccessKey(), LOCALSTACK.getSecretKey()));
try (S3Client s3 = S3Client.builder()
.endpointOverride(LOCALSTACK.getEndpoint())
.region(Region.of(LOCALSTACK.getRegion()))
.credentialsProvider(credentials)
.forcePathStyle(true).build()) {
s3.createBucket(request -> request.bucket("tc-bucket"));
assertEquals("tc-bucket", s3.listBuckets().buckets().get(0).name());
}
}
}mkdir -p src/test/java/example
mvn -q test输出应显示一个测试通过。缺少 token 时类初始化明确失败;测试不能回退到共享 LocalStack 或真实 AWS。并行测试使用随机且合法的资源名,测试结束由容器销毁实例,不需要自制清理框架。
Terraform 接入
先按 Terraform Install 为当前发行版配置 HashiCorp 官方仓库并安装 Terraform,然后执行:
terraform version
terraform version -json | jq -e \
'.terraform_version | split(".")[0] | tonumber >= 1'任一命令失败时停止。main.tf 同时声明最低版本:
创建 main.tf:
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 6.0" }
}
}
provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
s3_use_path_style = true
endpoints {
s3 = "http://localhost.localstack.cloud:4566"
sqs = "http://localhost.localstack.cloud:4566"
}
}
resource "aws_s3_bucket" "artifacts" { bucket = "ls-terraform-artifacts" }
resource "aws_sqs_queue" "events" {
name = "ls-terraform-events"
visibility_timeout_seconds = 30
}
output "bucket" { value = aws_s3_bucket.artifacts.bucket }
output "queue_url" { value = aws_sqs_queue.events.url }terraform init
terraform fmt -check
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
terraform output输出应包含 bucket 和本地 queue URL。随后用 AWS CLI 查询两个资源,不能只相信 Terraform state。真实 AWS job 使用另一份 provider 配置和 OIDC/STS,不允许继承 LocalStack endpoint 或 skip_* 选项。
CI 隔离与并发
每个 CI job 使用独立 Compose project name,资源名包含 run id,结束时统一 down:
jobs:
integration:
runs-on: ubuntu-latest
env:
COMPOSE_PROJECT_NAME: ls-${{ github.run_id }}-${{ github.run_attempt }}
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
AWS_ENDPOINT_URL: http://localhost.localstack.cloud:4566
steps:
- uses: actions/checkout@v4
- name: Start LocalStack
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: |
printf 'LOCALSTACK_AUTH_TOKEN=%s\n' "$LOCALSTACK_AUTH_TOKEN" > .env.local
chmod 600 .env.local
docker compose --env-file .env.local up -d --wait
- name: Run integration tests
run: ./mvnw -B verify
- name: Collect logs
if: always()
run: docker compose logs --no-color > localstack.log
- name: Stop LocalStack
if: always()
run: docker compose --env-file .env.local down --remove-orphans日志 artifact 不应包含 Auth Token、生产凭证、对象敏感内容或完整诊断包。并发任务不能共用固定 container name、固定 Compose project、同一 bind volume 或相同 AWS 资源名。
管理状态生命周期并切回真实 AWS
持久化、保存和恢复
需要在开发机重启后保留状态时设置。切换 persistence 或 snapshot 策略会让 Compose 重建容器,之前只在内存里的资源可能先消失,因此要等 init hook 重新建立基础 bucket,再创建一个不会被 hook 自动补回的本轮标记对象:
PERSISTENCE=1 \
SNAPSHOT_SAVE_STRATEGY=MANUAL \
SNAPSHOT_LOAD_STRATEGY=MANUAL \
docker compose --env-file .env.local up -d
for attempt in $(seq 1 60); do
curl -fsS http://localhost.localstack.cloud:4566/_localstack/init/ready \
>/dev/null && break
if [ "$attempt" -eq 60 ]; then
docker compose logs --tail 200 localstack
exit 1
fi
sleep 1
done
SNAPSHOT_KEY='snapshot-proof/o-snapshot-1001.json'
printf '{"orderId":"o-snapshot-1001","saved":true}\n' > snapshot-proof.json
aws s3 cp snapshot-proof.json "s3://ls-app-dev/$SNAPSHOT_KEY" \
--endpoint-url "$AWS_ENDPOINT_URL"
aws s3api head-object --bucket ls-app-dev --key "$SNAPSHOT_KEY" \
--endpoint-url "$AWS_ENDPOINT_URL"
curl -fsS -X POST \
http://localhost.localstack.cloud:4566/_localstack/state/s3/save \
| tee snapshot-save.ndjson
jq -s -e 'length == 1 and .[0].service == "s3" and .[0].status == "ok"' \
snapshot-save.ndjson响应必须明确给出 S3 的 status: ok。然后检查状态目录和磁盘,再重启同一个容器以清空内存状态:
find volume/state -maxdepth 2 -type f | head
du -sh volume
df -h volume
docker compose --env-file .env.local restart localstack
for attempt in $(seq 1 60); do
curl -fsS http://localhost.localstack.cloud:4566/_localstack/init/ready \
>/dev/null && break
if [ "$attempt" -eq 60 ]; then
docker compose logs --tail 200 localstack
exit 1
fi
sleep 1
done加载前先证明 marker 不在当前内存状态;如果这一步意外成功,就不能把后续结果算作 snapshot 恢复证据。随后手动加载 S3 snapshot,并重新查询对象内容:
if aws s3api head-object --bucket ls-app-dev --key "$SNAPSHOT_KEY" \
--endpoint-url "$AWS_ENDPOINT_URL" >/dev/null 2>&1; then
echo 'marker unexpectedly loaded before manual restore' >&2
exit 1
fi
curl -fsS -X POST \
http://localhost.localstack.cloud:4566/_localstack/state/s3/load \
| tee snapshot-load.ndjson
jq -s -e 'length == 1 and .[0].service == "s3" and .[0].status == "ok"' \
snapshot-load.ndjson
aws s3 cp "s3://ls-app-dev/$SNAPSHOT_KEY" - \
--endpoint-url "$AWS_ENDPOINT_URL" \
| jq -e '.orderId == "o-snapshot-1001" and .saved == true'保存策略决定写入延迟与崩溃窗口。默认 SNAPSHOT_SAVE_STRATEGY=SCHEDULED,由默认 15 秒的 SNAPSHOT_FLUSH_INTERVAL 周期刷新;间隔越长,异常退出可能丢失的窗口越大。ON_REQUEST 在修改 API 后立即保存,但服务加锁会放大写延迟;ON_SHUTDOWN 依赖正常退出。默认 SNAPSHOT_LOAD_STRATEGY=ON_REQUEST,首个业务请求会承担恢复延迟。上面的命令把 save/load 都改为 MANUAL,所以紧随其后的两个 state endpoint 才是这次实验的唯一保存与加载动作,调用方必须逐个检查服务状态。
状态保存可能阻塞对应服务请求,状态格式也可能跨版本不兼容;升级前保留旧镜像和旧 volume 副本,并在隔离目录验证,不能直接覆盖唯一状态。自动化集成测试通常不启用持久化,每次从空状态运行更容易发现缺失的 IaC、初始化依赖和测试污染。
进入真实 AWS 前切断本地环境
真实 AWS 契约必须在新的干净 shell 或独立 CI job 中运行。所有以 AWS_ENDPOINT_URL 开头的全局或服务级 endpoint 都必须为空;AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true 同时禁止共享配置和 profile 中的 endpoint 覆盖。
隔离 profile 分支
profile 分支先拒绝残留 endpoint 和 test/test,再清除会覆盖 profile 的环境凭证:
if env | grep -q '^AWS_ENDPOINT_URL'; then exit 1; fi
test "${AWS_ACCESS_KEY_ID:-}" != test || exit 1
test "${AWS_SECRET_ACCESS_KEY:-}" != test || exit 1
: "${AWS_PROFILE:?set isolated AWS profile}"
: "${EXPECTED_AWS_ACCOUNT_ID:?set expected account}"
: "${CONTRACT_ROLE_NAME:?}" "${AWS_BUCKET:?}" "${AWS_KEY:?}"
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
unset AWS_WEB_IDENTITY_TOKEN_FILE AWS_ROLE_ARN AWS_ROLE_SESSION_NAME
export AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true首个 API 只能是显式 profile 的 STS account gate:
account="$(aws --profile "$AWS_PROFILE" sts get-caller-identity \
--query Account --output text)"
test "$account" = "$EXPECTED_AWS_ACCOUNT_ID" || exit 1
ROLE_ARN="arn:aws:iam::$account:role/$CONTRACT_ROLE_NAME"
OBJECT_ARN="arn:aws:s3:::$AWS_BUCKET/$AWS_KEY"
export ROLE_ARN OBJECT_ARNCI OIDC 分支
OIDC job 不设置 profile。平台登录步骤取得临时 access key、secret 和 session token 后,执行另一条独立门:
if env | grep -q '^AWS_ENDPOINT_URL'; then exit 1; fi
test -z "${AWS_PROFILE:-}" || exit 1
test "${AWS_ACCESS_KEY_ID:-}" != test || exit 1
test "${AWS_SECRET_ACCESS_KEY:-}" != test || exit 1
: "${AWS_ACCESS_KEY_ID:?}" "${AWS_SECRET_ACCESS_KEY:?}"
: "${AWS_SESSION_TOKEN:?temporary credentials required}"
: "${EXPECTED_AWS_ACCOUNT_ID:?}" "${CONTRACT_ROLE_NAME:?}"
: "${AWS_BUCKET:?}" "${AWS_KEY:?}"
export AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true
account="$(aws sts get-caller-identity --query Account --output text)"
test "$account" = "$EXPECTED_AWS_ACCOUNT_ID" || exit 1
ROLE_ARN="arn:aws:iam::$account:role/$CONTRACT_ROLE_NAME"
OBJECT_ARN="arn:aws:s3:::$AWS_BUCKET/$AWS_KEY"
export ROLE_ARN OBJECT_ARN两条分支互斥。下文真实 AWS 故障命令展示 profile 分支;OIDC job 在通过自己的 account gate 后省略 --profile。任一 endpoint、假凭证或账户不匹配时,都不允许执行后续 IAM、S3、创建或删除命令。
监控、容量和安全
健康检查只说明 Gateway 和服务状态,不代表测试业务正确。至少同时观察容器、日志、资源、状态目录和业务探针:
docker compose ps
docker compose logs --since 10m localstack
docker stats --no-stream
docker ps --filter label=com.localstack.service=lambda
du -sh volume
df -h volume容量预算要包括主镜像、Lambda 运行时镜像、服务依赖镜像、volume、临时文件、构建产物和并发子容器。docker system prune 会影响宿主其他项目,不能作为默认清理命令。
Gateway 只映射到 127.0.0.1。Docker socket 接近宿主 root 权限,共享 runner 应使用隔离 VM、专用 daemon 或平台支持的 Kubernetes executor。本地测试只使用假 AWS 凭证;真实 AWS 契约 job 使用专用测试账户、短期 OIDC/STS 身份、最小权限和预算限制,并显式清空 LocalStack endpoint。
升级、回退与清理
升级前记录当前身份、镜像和服务状态:
curl -fsSI http://localhost.localstack.cloud:4566/_localstack/health \
| grep -i '^x-localstack:'
docker compose images
docker image inspect localstack/localstack:2026.08.0 \
--format '{{.Id}} {{.RepoDigests}}'升级测试先保存状态并拒绝任一服务失败:
curl -fsS -X POST "$AWS_ENDPOINT_URL/_localstack/state/save" \
| jq -se 'length > 0 and all(.[]; .status == "ok")'
aws s3api list-buckets --endpoint-url "$AWS_ENDPOINT_URL" \
--query 'Buckets[].Name' --output json | jq -S . > baseline-s3.json
aws lambda list-functions --endpoint-url "$AWS_ENDPOINT_URL" \
--query 'Functions[].FunctionName' --output json | jq -S . > baseline-lambda.json
aws dynamodb list-tables --endpoint-url "$AWS_ENDPOINT_URL" \
--query TableNames --output json | jq -S . > baseline-ddb.json停止旧实例后再复制稳定 volume。新版本只使用副本、独立容器名和替代宿主端口:
: "${TARGET_VERSION:?set approved exact CalVer}"
set -a; . ./.env.local; set +a
upgrade_dir="$(mktemp -d "$PWD/upgrade.XXXXXXXX")"
docker compose --env-file .env.local stop localstack
cp -a volume "$upgrade_dir/volume"
test -z "$(docker ps -aq -f name=^/localstack-upgrade-test$)" || exit 1
docker run -d --name localstack-upgrade-test \
-p 127.0.0.1:14566:4566 \
-e LOCALSTACK_AUTH_TOKEN -e PERSISTENCE=1 \
-v "$upgrade_dir/volume:/var/lib/localstack" \
-v /var/run/docker.sock:/var/run/docker.sock \
"localstack/localstack:$TARGET_VERSION"新实例必须先健康和版本正确,再比较关键资源基线:
UPGRADE_ENDPOINT=http://127.0.0.1:14566
curl -fsS "$UPGRADE_ENDPOINT/_localstack/health" | jq .
curl -fsSI "$UPGRADE_ENDPOINT/_localstack/health" | grep -i x-localstack
aws s3api list-buckets --endpoint-url "$UPGRADE_ENDPOINT" \
--query 'Buckets[].Name' --output json | jq -S . | diff - baseline-s3.json
aws lambda list-functions --endpoint-url "$UPGRADE_ENDPOINT" \
--query 'Functions[].FunctionName' --output json | jq -S . | diff - baseline-lambda.json
aws dynamodb list-tables --endpoint-url "$UPGRADE_ENDPOINT" \
--query TableNames --output json | jq -S . | diff - baseline-ddb.json还应在新实例执行 S3 读写、SQS 发送接收、Lambda invoke 和 DynamoDB 条件更新。任一步失败时删除测试实例并恢复原实例;原 volume 从未被新版本挂载:
docker rm -f localstack-upgrade-test
docker compose --env-file .env.local start localstack
curl -fsS "$AWS_ENDPOINT_URL/_localstack/health" | jq .
aws s3api list-buckets --endpoint-url "$AWS_ENDPOINT_URL" \
--query 'Buckets[].Name' --output json | jq -S . | diff - baseline-s3.json只有全部基线和业务契约通过,才修改正式 Compose 版本;仍保留旧镜像与原 volume 直到回退窗口结束。测试状态可重建时,优先从 IaC 和 seed 创建新状态。
删除本轮资源后停止容器:
aws lambda delete-function --function-name ls-order-handler \
--endpoint-url "$AWS_ENDPOINT_URL"
aws dynamodb delete-table --table-name ls-orders \
--endpoint-url "$AWS_ENDPOINT_URL"
aws s3 rm s3://ls-app-dev --recursive --endpoint-url "$AWS_ENDPOINT_URL"
aws s3api delete-bucket --bucket ls-app-dev --endpoint-url "$AWS_ENDPOINT_URL"
aws sqs delete-queue --queue-url "$QUEUE_URL" --endpoint-url "$AWS_ENDPOINT_URL"
docker compose --env-file .env.local down --remove-orphans只清理当前实验 bind volume 时,先使用固定项目根并验证路径:
project_root="$(realpath "$PWD")"
actual="$(realpath -m "$PWD/volume")"
expected="$project_root/volume"
test "$actual" = "$expected" || exit 1
rm -rf -- "$actual"生产目录和共享 runner 不使用这条删除命令。新 CI job 优先使用新临时目录,而不是编写复杂脚本猜测资源所有权。
四、问题处理
统一判断流程
先确认当前请求来自宿主机、应用容器还是 Lambda 子容器,再记录实际 endpoint、region、STS account、LocalStack 版本和 Compose project。随后按 Gateway 与许可、init 状态、目标资源、计算子容器、持久化目录、真实 AWS 契约的顺序推进。上一层证据没有成立时,不跳到下一层加重试;否则 endpoint 错误、旧状态污染和真实权限拒绝会被混成同一个“LocalStack 不稳定”。
最小现场快照只需要直接命令,不需要另造诊断框架:
docker compose ps
curl -fsS "$AWS_ENDPOINT_URL/_localstack/health" | jq
curl -fsS "$AWS_ENDPOINT_URL/_localstack/init/ready" | jq
curl -fsS "$AWS_ENDPOINT_URL/_localstack/info" | jq
aws sts get-caller-identity --endpoint-url "$AWS_ENDPOINT_URL"
docker compose logs --since 10m localstack从本地 Gateway 追到资源与计算状态
Gateway、许可与请求入口
Auth Token 或许可激活失败
现象
容器启动后退出、持续 unhealthy,日志出现 token、license、entitlement 或连接许可服务器失败。
影响
Gateway 无法使用,或者计划内服务未激活,所有后续 AWS API 测试失去意义。
常见根因
Token 未注入、包含换行、已被重置、没有分配许可、CI secret 作用域错误,或者宿主无法访问许可和静态资源域名。
定位顺序
先确认 Compose 能解析变量,再看容器退出码和完整启动日志,最后检查宿主和容器的 DNS、HTTPS 出站与账户许可。
定位命令
docker compose --env-file .env.local config --quiet
docker compose ps -a
docker compose logs --tail 200 localstack
curl -fsS https://api.localstack.cloud/v1/health
dig api.localstack.cloud输出判断
配置解析必须成功,健康接口应返回 HTTP 200,DNS 应为 NOERROR。日志中的 license reason 用于区分 token 无效、许可未分配和网络失败。
解决步骤
重新生成正确类型的 token,通过本地 0600 文件或 CI secret 注入;分配对应计划;修复代理、DNS 和 TCP 443 出站。泄露 token 必须先重置。
验证
重新创建容器,健康接口成功,并使用一个计划内服务完成创建和查询。
预防
开发和 CI 使用不同 token,定期轮换,secret 不进入日志、镜像和仓库;CI 启动前执行 config gate。
容器 unhealthy 或 4566 无法访问
现象
curl 连接拒绝、Compose 显示 unhealthy,或者端口没有监听。
影响
所有 CLI、SDK、IaC 和初始化请求失败。
常见根因
容器退出、端口冲突、Docker daemon 异常、健康启动时间不足、镜像架构不匹配、资源不足或绑定地址错误。
定位顺序
检查容器状态和退出码,再看端口与日志,然后确认镜像架构、CPU、内存和磁盘。
定位命令
docker compose ps -a
docker inspect "$(docker compose ps -q localstack)" \
--format '{{.State.Status}} {{.State.ExitCode}} {{.State.Health.Status}}'
ss -lntp | grep 4566
docker compose logs --tail 200 localstack
free -h
df -h输出判断
容器应为 running/healthy,退出码为 0,127.0.0.1:4566 应监听。OOM、no space、address already in use 和 traceback 分别指向资源、端口或程序错误。
解决步骤
释放冲突端口和磁盘,增加合理启动时间与资源,拉取匹配架构的固定镜像;镜像回归时回退已验证 patch。
验证
健康接口、版本响应头、STS 与 S3 smoke test 都通过。
预防
CI 固定镜像版本,使用健康等待而不是固定 sleep,监控 runner 磁盘和内存。
endpoint、DNS 或代理配置错误
现象
宿主可以访问 LocalStack,但容器内应用失败;或者请求意外访问真实 AWS。
影响
测试出现连接失败、数据污染,严重时在真实账户创建资源和产生费用。
常见根因
容器内使用 localhost、域名被 DNS rebind protection 拦截、代理未排除本地地址、endpoint 未注入或透明 DNS 范围错误。
定位顺序
确认代码运行位置,再检查名称解析、TCP、响应头和 SDK endpoint,最后核对代理环境。
定位命令
dig localhost.localstack.cloud
curl -v http://localhost.localstack.cloud:4566/_localstack/health
docker compose exec localstack getent hosts localhost.localstack.cloud
env | grep -iE 'http_proxy|https_proxy|no_proxy|AWS_ENDPOINT_URL'输出判断
宿主域名应解析到 loopback,响应必须带 x-localstack。应用容器中的 localhost 指向自身,通常应使用可解析的服务名。
解决步骤
宿主使用本地域名或 127.0.0.1;应用容器使用 Compose 服务名;把本地域名加入 NO_PROXY,并为 S3 选择匹配寻址方式。
验证
从真实应用运行位置请求健康接口和 STS,确认版本后再执行业务 API。
预防
每个环境维护明确 endpoint 表,启动时打印脱敏 endpoint 和账户,禁止静默回退到 AWS。
资源身份与数据状态
S3 签名、bucket 或预签名 URL 错误
现象
S3 返回 SignatureDoesNotMatch、NoSuchBucket、DNS 或 TLS 错误。
影响
上传下载和浏览器直传失败,不同客户端结果不一致。
常见根因
虚拟主机与 path-style 混用、endpoint 未带 s3. 前缀、region 不一致、签名和访问 hostname 不同、代理重写 Host 或证书不覆盖区域。
定位顺序
核对 bucket、region、endpoint、Host 和寻址配置,再用 curl 查看预签名响应。
定位命令
aws s3api head-bucket --bucket ls-app-dev \
--endpoint-url "$AWS_ENDPOINT_URL" --debug 2> s3-debug.log
grep -E 'endpoint|Host:|Authorization:' s3-debug.log | head
curl -v "$url" -o /tmp/presigned-body输出判断
签名 endpoint、请求 Host 和 URL hostname 必须一致。使用 localhost:4566 时通常需要 path-style。
解决步骤
统一 endpoint 和 region;SDK 显式启用 path-style,或切换 S3 专用域名;生成和消费 URL 使用同一 hostname。
验证
CLI、SDK 和 curl 对同一对象的内容一致;浏览器场景再验证 CORS。
预防
集中维护 S3 client factory 和预签名域名,禁止在调用点临时拼 endpoint。
资源创建成功但查询不到
现象
前一个命令成功,后一个客户端却报告资源不存在。
影响
初始化、测试和清理链断裂,团队误判状态丢失。
常见根因
region、账户、endpoint、Compose project 或容器实例不同;容器已重建且未启用持久化。
定位顺序
从两个客户端输出 endpoint、region 和 STS 身份,再确认同一容器版本和 project。
定位命令
aws configure list
aws sts get-caller-identity --endpoint-url "$AWS_ENDPOINT_URL"
docker compose ls
docker compose ps
curl -fsSI "$AWS_ENDPOINT_URL/_localstack/health" | grep -i x-localstack输出判断
endpoint、账户、region 和实例必须一致。容器变化且 PERSISTENCE=0 时旧状态消失属于预期。
解决步骤
统一 profile、region 和 endpoint;需要状态时启用并验证 persistence,否则从 IaC 和初始化脚本重建。
验证
shell、应用和测试进程都能查询同一资源,重建行为符合状态策略。
预防
日志记录 endpoint、region、account 与版本,资源名包含 run id。
Lambda 计算运行时
Lambda Pending 或 Docker not available
现象
函数一直 Pending、waiter 超时,日志出现 Docker not available。
影响
函数和事件链测试无法执行。
常见根因
未挂载 socket、权限不足、运行时镜像拉取失败、架构不匹配、代理或宿主资源不足。
定位顺序
先查函数状态原因,再查 LocalStack 日志、socket 和子容器,最后检查镜像与资源。
定位命令
aws lambda get-function-configuration --function-name ls-order-handler \
--endpoint-url "$AWS_ENDPOINT_URL"
docker compose logs --tail 300 localstack | grep -iE 'lambda|docker|runtime'
docker compose exec localstack test -S /var/run/docker.sock
docker ps -a --filter label=com.localstack.service=lambda输出判断
函数应为 Active。socket 不存在、permission denied、image pull failed、exec format error 分别对应挂载、权限、网络或架构。
解决步骤
挂载正确 socket,修复受控 daemon 权限;配置代理;预拉取匹配架构镜像;资源不足时降低并发。
验证
函数 Active,直接 invoke 和事件源调用都成功,并包含本轮 request id。
预防
CI 执行最小 Lambda smoke test,监控磁盘和子容器,不保留废弃 executor 配置。
初始化、持久化与队列状态
初始化钩子未执行或重复失败
现象
容器 healthy,但预期资源不存在;日志显示 permission、换行或命令错误。
影响
测试缺少前置资源,或者每次重启重复创建并报冲突。
常见根因
脚本目录、权限、CRLF、挂载错误,脚本不幂等,错误被吞掉,或只 restart 没有重建容器。
定位顺序
检查容器内文件、权限和格式,再看启动日志,最后查询资源。
定位命令
docker compose exec localstack ls -l /etc/localstack/init/ready.d
file init/ready.d/10-bootstrap.sh
docker compose logs localstack | grep -iE 'init|ready|hook|error'
aws s3api head-bucket --bucket ls-app-dev --endpoint-url "$AWS_ENDPOINT_URL"输出判断
脚本应可执行、为 Unix 文本,日志无 hook error,资源查询成功。
解决步骤
修正挂载与 LF,执行 chmod 0755;先查再建;删除吞错逻辑;强制重建容器。
验证
连续两次重建成功,资源不重复,任一命令失败时日志明确失败。
预防
hook 保持短小幂等,复杂 IaC 交给 Terraform,CI 独立验证前置资源。
持久化丢失或版本不兼容
现象
重启后资源消失,或新镜像拒绝加载旧状态。
影响
开发数据和调试现场不可用,升级测试被脏状态阻塞。
常见根因
未启用 persistence、volume 路径错误、project 不同、异常终止,或状态格式不兼容。
定位顺序
确认配置和 mount,再检查 state、保存日志和兼容信息,最后查询业务资源。
定位命令
docker compose config | grep -E 'PERSISTENCE|/var/lib/localstack'
docker inspect "$(docker compose ps -q localstack)" \
--format '{{json .Mounts}}' | jq
find volume/state -maxdepth 2 -type f | head
docker compose logs --tail 300 localstack | grep -iE 'state|snapshot|compat'输出判断
目录必须挂载到 /var/lib/localstack,state 应有服务数据。兼容规则拒绝时不能把空查询当成恢复成功。
解决步骤
回到旧镜像读取;在副本测试升级;可重建数据优先由 IaC 和 seed 生成,不关闭规则覆盖唯一 volume。
验证
隔离恢复后资源、对象内容、队列属性和业务对账符合保存前基线。
预防
自动化默认无状态;长期状态记录镜像版本;升级保留旧镜像与 volume 副本。
SQS 消息重复、不可见或不消费
现象
消息发送成功但 receive 为空、重复出现,或 Lambda 事件源不处理。
影响
异步测试不稳定,幂等和重试缺陷被掩盖。
常见根因
队列来自不同实例、visibility timeout 未结束、另一消费者已取得、handle 失效、mapping 未 Enabled 或过滤不匹配。
定位顺序
确认队列属性,再检查消息数量、mapping、消费者日志和 body。
定位命令
aws sqs get-queue-attributes --queue-url "$QUEUE_URL" \
--attribute-names All --endpoint-url "$AWS_ENDPOINT_URL"
aws lambda list-event-source-mappings --function-name ls-order-handler \
--endpoint-url "$AWS_ENDPOINT_URL"
docker compose logs --tail 200 localstack | grep -iE 'sqs|event source|lambda'输出判断
可见和不可见消息数区分未消费与处理中;mapping 必须 Enabled,处理结果不应持续报错。
解决步骤
统一 endpoint 和 queue URL;等待 timeout 或正确删除;修正 mapping、过滤和函数错误;消费者实现幂等。
验证
发送唯一业务 id,重复投递不产生重复副作用,队列最终清空。
预防
覆盖重复投递、超时重试和坏消息,真实 AWS 再验证 DLQ、并发和节流。
检查共享 CI 与真实 AWS 边界
IAM 契约差异
本地 IAM 通过但 AWS AccessDenied
现象
LocalStack API 成功,AWS 被 IAM、KMS、bucket policy、trust policy 或 SCP 拒绝。
影响
本地绿色无法阻止真实权限故障。
常见根因
LocalStack 未覆盖对应条件,测试身份权限过大,缺少跨账户、KMS、boundary 或组织策略。
定位顺序
保存 AWS request id,核对身份、ARN、identity/resource/KMS policy 与组织限制。
定位命令
if env | grep -q '^AWS_ENDPOINT_URL'; then exit 1; fi
test "${AWS_ACCESS_KEY_ID:-}" != test || exit 1
test "${AWS_SECRET_ACCESS_KEY:-}" != test || exit 1
: "${AWS_PROFILE:?}" "${EXPECTED_AWS_ACCOUNT_ID:?}"
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
unset AWS_WEB_IDENTITY_TOKEN_FILE AWS_ROLE_ARN AWS_ROLE_SESSION_NAME
export AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true
account="$(aws --profile "$AWS_PROFILE" sts get-caller-identity \
--query Account --output text)"
test "$account" = "$EXPECTED_AWS_ACCOUNT_ID" || exit 1
aws --profile "$AWS_PROFILE" sts get-caller-identity
aws --profile "$AWS_PROFILE" iam simulate-principal-policy \
--policy-source-arn "$ROLE_ARN" \
--action-names s3:GetObject --resource-arns "$OBJECT_ARN"输出判断
STS 必须是隔离测试角色;模拟 allowed 仍不能覆盖 resource policy、KMS、SCP 和运行时 context。
解决步骤
按最小权限修正 identity/resource/KMS/trust 配置,不用通配权限作为长期修复。
验证
隔离 AWS 中允许身份成功、越权身份失败,并保留 CloudTrail 证据。
预防
关键 IAM/KMS 流程保留真实 AWS 门禁,本地只验证应用错误处理。
CI 隔离与宿主资源
CI 并发互相污染
现象
单独通过,并发时资源已存在、读取他人数据、误删资源或端口冲突。
影响
测试随机失败,一个 job 可能破坏另一个 job。
常见根因
固定 project、container、端口、volume 和资源名,多个 job 共用 daemon。
定位顺序
对比 project、容器 ID、endpoint、资源名、run id、mount 和端口。
定位命令
docker compose ls
docker ps --format '{{.ID}} {{.Names}} {{.Ports}}'
docker inspect "$(docker compose ps -q localstack)" \
--format '{{json .Mounts}}' | jq
printf '%s\n' "$COMPOSE_PROJECT_NAME" "$CI_JOB_ID" "$AWS_ENDPOINT_URL"输出判断
每个 job 应有唯一 project、容器、volume 和资源命名空间。
解决步骤
加入 run id,使用临时目录和独立 daemon/VM;只 down 本 project,不做全局 prune。
验证
多轮并行,并让一个 job 失败,其他 job 仍完成且资源不受影响。
预防
CI 模板统一隔离标识,禁止固定 container name 和共享持久化目录。
磁盘、内存或子容器耗尽
现象
服务超时、Lambda 无法启动、OOMKill、镜像失败或 no space left。
影响
测试大面积失败,状态可能不完整,宿主其他项目也受影响。
常见根因
子容器残留、镜像和日志增长、并发过高、Docker 根目录空间不足或内存过小。
定位顺序
先看 OOM 和退出码,再查内存、文件系统、Docker 占用、子容器和 volume。
定位命令
docker inspect "$(docker compose ps -q localstack)" \
--format '{{.State.OOMKilled}} {{.State.ExitCode}}'
free -h
df -h
docker system df
docker ps -a --filter label=com.localstack.service=lambda
du -sh volume输出判断
OOM、磁盘满、镜像增长和大量停止子容器分别指向内存、磁盘和运行环境泄漏。
解决步骤
停止测试并保留日志;只清理本项目资源;降低并发、限制数据或扩容 runner。
验证
从干净实例运行完整测试,容器不 OOM,磁盘和子容器数量回到基线。
预防
监控 runner,设置超时和并发上限,优先使用可销毁临时 runner。
TLS 与企业代理
HTTPS 证书或企业代理错误
现象
HTTPS 返回 hostname mismatch、unknown authority,或许可与运行时依赖无法下载。
影响
SDK、浏览器、Lambda runtime 和许可激活失败。
常见根因
区域不在证书覆盖、代理替换证书、缺少企业 CA、NO_PROXY 不完整、系统时间或 hostname 错误。
定位顺序
确认 hostname 和 region,再查证书链、时间、代理以及宿主与容器差异。
定位命令
openssl s_client -connect s3.us-east-1.localhost.localstack.cloud:4566 \
-servername s3.us-east-1.localhost.localstack.cloud </dev/null
date -u
env | grep -iE 'proxy|ca_bundle|cert'
docker compose logs --tail 200 localstack | grep -iE 'ssl|tls|certificate|proxy'输出判断
证书 SAN 必须覆盖 hostname,链验证成功,系统时间正确。代理签发者异常说明需安装企业 CA。
解决步骤
使用覆盖的 region/hostname,或本地改用 HTTP;注入企业 CA;完善 NO_PROXY,不全局关闭验证。
验证
curl、CLI、SDK 和 Lambda 内部请求都能访问,且没有 insecure 配置。
预防
集中管理开发 CA 和代理,TLS hostname 纳入 CI,跟踪官方覆盖变化。
真实 AWS 复核
LocalStack 通过但真实 AWS 失败
现象
本地测试绿色,隔离 AWS job 在 API、时序、权限、配额或网络上失败。
影响
没有真实云门禁时,差异会进入预生产或生产。
常见根因
API 覆盖不完整、错误码和一致性不同、IAM/KMS/网络未真实模拟、AWS 变化、断言过弱或使用内部接口。
定位顺序
比较两边请求、身份、region、资源、响应和 request id,再查覆盖页与 AWS 文档。
定位命令
if env | grep -q '^AWS_ENDPOINT_URL'; then exit 1; fi
test "${AWS_ACCESS_KEY_ID:-}" != test || exit 1
test "${AWS_SECRET_ACCESS_KEY:-}" != test || exit 1
: "${AWS_PROFILE:?}" "${EXPECTED_AWS_ACCOUNT_ID:?}"
: "${AWS_BUCKET:?}" "${AWS_KEY:?}"
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
unset AWS_WEB_IDENTITY_TOKEN_FILE AWS_ROLE_ARN AWS_ROLE_SESSION_NAME
export AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true
account="$(aws --profile "$AWS_PROFILE" sts get-caller-identity \
--query Account --output text)"
test "$account" = "$EXPECTED_AWS_ACCOUNT_ID" || exit 1
aws --profile "$AWS_PROFILE" --debug s3api head-object --bucket "$AWS_BUCKET" \
--key "$AWS_KEY" 2> aws-debug.log输出判断
AWS job 不应设置 LocalStack endpoint,身份必须是隔离测试角色;error code、request id 和 CloudTrail 用于分类。
解决步骤
修复业务或 IaC 满足 AWS 契约,把差异加入真实 AWS 测试,不通过放宽本地断言掩盖问题。
验证
同一核心契约在 LocalStack 和隔离 AWS 通过,越权与错误输入均被验证。
预防
保留小而关键的 AWS 契约集,升级 LocalStack、SDK 或 provider 时同时执行。
恢复完成判据
恢复不能以“容器重新 healthy”结束。同一轮业务 ID 必须在当前 endpoint、account、region 和 Compose project 中重新完成 S3 写入读取、SQS 收发删除以及文章涉及的 Lambda 或 DynamoDB 路径;init 状态成功,日志不再出现本轮错误,队列可见与不可见消息回到基线,临时子容器和测试资源完成清理。若故障跨越真实 AWS,还要在隔离账户用同一核心契约取得允许身份成功、越权身份失败和目标资源无残留三类结果,才算恢复完成。
