gherkai-deploy-aws
gherkai 的 AWS 后端供给 provider 包:Python CDK 定义 --backend cloud 所需的全部云资源(DDB / S3 / ECS / ECR / IAM / VPC + 无状态跑批的 Stream·Lambda·EventBridge,ADR 0033/0034),以及 worker 镜像的推送与注册(ADR 0038)。
- 发行名
gherkai-deploy-aws· import 名gherkai_deploy_aws(ADR 0037 决策 2a/6)。 - 不单独安装:随 CLI 的 extra 进来——
uv tool install 'gherkai[deploy-aws]';命令面是gherkai deploy/gherkai destroy,本包经 entry point groupgherkai.deploy被发现(装一个 provider 时无需--provider)。 - 只有部署方需要装它:改云端环境是云端写操作;只提交 run 的人不需要(ADR 0037 决策 2c)。
- Node ≥ 22 必须在 PATH:
aws-cdk-lib是 jsii 绑定(import 即起 node 子进程),cdk CLI 本身也是 npm 物。命令用 PATH 上的cdk,没有则回落npx -y aws-cdk@2。缺 Node 时命令报一句人话并退 2,不吐 jsii 堆栈。
「clone repo + 裸
cdk deploy」不再是部署形态(ADR 0037 决策 6):那条路要求源码树在手、相对路径拼 Lambda asset、且把 vpc context 的坑外露给每个使用者。现在 IaC 随 wheel 分发,gherkai deploy在临时工作目录生成cdk.json(app指向python -m gherkai_deploy_aws.app)后调 cdk CLI。contributor 想看模板走gherkai deploy --synth-only DIR,别裸跑python -m gherkai_deploy_aws.app(它缺-c version=会 fail-fast,这是有意的)。
建什么
一套 CloudFormation stack,stack 名 BackendStack-<prefix 去尾横线>(可按 prefix 多实例化,支持 prod-/stage- 多环境并存):
- DynamoDB:
{prefix}runs(控制面/RunStore,带按status的稀疏 GSIstatus-index——只有 STATE item 有顶层status,投影含worker_task_def_arns,供 worker revision 清理的「在跑 run 安全阀」Query,ADR 0038)+{prefix}events(events-out,开expires_atTTL)——两表均开 DynamoDB Stream(NEW_IMAGE),作为无状态跑批事件驱动链的触发源(见下「事件驱动推进」,ADR 0034) - S3:
{prefix}artifacts(判定结果 / 报告 / offload / job-in / 引擎产物,按 key 前缀分片)+ lifecycle 规则expire-job-in(按对象 taggherkai=job-in7 天过期——job-in 的 key 里run_id在中间,纯前缀 filter 框不住且会误伤判定真值与报告) - ECS:
{prefix}cluster+ 2 个 Fargate task-def({prefix}novaact-worker/{prefix}midscene-worker) - ECR:2 个 repo(repo 名 == task-def family 名,
names.ecr_repo_name;镜像由部署方gherkai deploy push-worker推,CDK 只建 repo、synth 不触发 docker build)。不设任何 lifecycle 规则——重推同名 variant 会把旧 tag 顶成 untagged,而在跑 run 的旧 task-def revision 正按 digest 指着那一层,untagged 过期规则会静默删掉它(ADR 0038 护栏;代价是永久留一层 untagged,回收与delete-worker同批设计) - IAM:每引擎一个最小权限 task role + 共享 execution role + 3 个 Lambda 执行角色 + job timeout 的 Scheduler 执行 role
{prefix}timeout-scheduler - VPC + SSM 网络:worker 网络(subnet/sg)+ 把它们的 ID 写进
/{prefix}backend/subnets、/{prefix}backend/security-groups(cli 读) - SSM 部署戳(都是 stack 资源、不是命令事后
put_parameter——与部署事务同生死、回滚不留错值,ADR 0037 决策 6):/{prefix}backend/version:后端版本戳,供提交侧 preflight 比对 CLI 版本(skew 三态,ADR 0037 决策 7)/{prefix}backend/vpc:生效的 VPC 档,供下次gherkai deploy三态比对(见下「VPC 三档」)/{prefix}backend/worker-template/<engine>:worker task-def 的模板 revision ARN(Ref返回带 revision 的 ARN),push-worker从它复制模板(ADR 0038 四步第 1 步)
事件驱动推进(无状态跑批,ADR 0034)
submit 提交完即走、进程不驻留,run 的推进改由云上事件链自我驱动(stack._reconcile_lambdas)。同步 run 路径不消费 Stream、仍走 Query 轮询,与此链解耦(ADR 0024)。
- 三个 Lambda(同一份打包 asset,
handler入口不同——见下「Lambda 打包」):{prefix}kicker(踢启器):{prefix}runs表 Stream 的 INSERT 触发(submit的create_run写 definition)→ 冷启动起首批 task;也被 clistatus --wait直接 invoke 做 kickoff。handler=reconciler.kicker_handler。{prefix}reconciler:{prefix}events表 Stream 触发(workerPutItem执行事件 / 退出观察者写task_exited)→reconcile.tick推进 + finalize 聚合。handler=reconciler.handler。{prefix}exit-observer(退出观察者):ECS TaskSTOPPED事件触发 → 写task_exited事件(薄;只 events 表PutItem)。handler=exit_observer.handler。- kicker/reconciler 共享起 task 的全套权限与装配(
RunTask/PassRole/ 表桶读写 +SUBNETS/SECURITY_GROUPS/MAX_CONCURRENCY,另加ssm:GetParameter*于/{prefix}backend/*);分工 = kicker「让 run 动起来」、reconciler「推着走」。MAX_CONCURRENCY(当前8,两侧须同值)是部署侧 per-run 并发 cap、不是真源——每个 run 并行几个 job 由提交侧的submit --max-concurrency随 definition 声明,kicker/reconciler 取min(声明, cap);cap 在此是因为 task 烧的是部署方账单,local 档无 cap(ADR 0034 机制四)。 - 两个推进器起 task 用的是 definition 里解析好的显式 task-def revision(不是 family 最新 ACTIVE——那会让任何一次 push 劫持别人的 variant)。没有该字段的旧 definition(升级窗口内在跑的 run / 旧 CLI 提交的 run)走兼容回落:读 SSM
worker-default+worker-image/<engine>/<后端版本>-<默认 variant>解析 revision——这就是它们要 SSM 读权限的原因。不回落模板 revision:模板的镜像栏是latest占位,全新 prefix 上根本拉不到(ADR 0038 被拒方案)。
- EventBridge rule
{prefix}ecs-stopped:按source=aws.ecs+ECS Task State Change+lastStatus=STOPPED+ 本 cluster 的clusterArn过滤(不误触别的负载)→ 打到{prefix}exit-observer。 - Event source mappings:
{prefix}events表 Stream → reconciler;{prefix}runs表 Stream → kicker(带eventName=INSERT∧detached=truefilter,只让 detached 的create_run触发冷启动;reconciler 之后写 runs 表的MODIFY不自触发放大,见 ADR 0034 被拒方案)。 - job timeout 到点触发器(ADR 0034「job timeout」节):IAM role
{prefix}timeout-scheduler(scheduler.amazonaws.comassume、只准 invoke kicker——授权写确定性 kicker ARN 串而非资源引用,免 role↔function 互引成环)+ EventBridge Scheduler 的 one-time schedule 名字空间{prefix}job-timeout-*(default group,ActionAfterCompletion=DELETE到点自删、idle 零成本)+ 注给 reconciler/kicker 的KICKER_ARN/SCHEDULER_ROLE_ARNenv。改 prefix 时这两个名字随之变。
Lambda 打包(stack._build_lambda_asset)
三个 Lambda 共用一个 asset:本包 gherkai_deploy_aws/lambdas/ 的两个 handler 源摊在 zip 根(故 exit_observer 里 from reconciler import … 这条平级 import 成立;也因此 handler 源不做成本包的子包)+ 从当前 venv 已安装位置复制的 gherkai_runtime / gherkai_core / gherkin / packaging / typing_extensions。不打 gherkai_cli(Lambda 不背 argparse/render,ADR 0016「演进」节);boto3 由 Lambda runtime 自带、不打。
两条踩出来的规矩:
- 来源必须是「已安装包」,不是仓库相对路径、也不是联网
pip install(ADR 0037 决策 6)。相对路径只在 monorepo 里成立,wheel 用户的 site-packages 没那种布局;联网装会在离线环境断,且装到的是 PyPI 上的某个版本而非运行中这一份——dev 版根本不在 PyPI 上,contributor 部署自己的 dev 版会直接断。 - 清单是手写的,所以要有一条真 import 的测试守着:
typing_extensions是gherkin-official>=42的传递依赖(gherkin/parser_types.py无条件 import 它),旧的pip install --target顺带装上、改成「按名复制」后就漏了——单测全绿,真 synth 出的 asset 一 import 就ModuleNotFoundError。现在tests/test_lambda_asset.py::test_asset_imports_with_only_stdlib_beside_it在剥掉 site-packages 的子进程里真 import 一遍 asset,加/换依赖时它是判据。
asset 落进 gherkai deploy 的临时工作目录(GHERKAI_LAMBDA_ASSET_DIR,命令用完即删),不再写进仓库或包目录。
prefix 契约(关键)
--prefix(默认 gherkai-,也认 AWS_RESOURCE_PREFIX)必须与 gherkai run / submit 的 --prefix 一致——CDK 建的资源名 = cli 推导的默认名(gherkai_deploy_aws/names.py re-export 产品本体 gherkai_runtime.names,与 cli 同源)。不一致 → cli 连不上资源、preflight 报错点名 prefix。
命名规则真同源:names.py 直接 re-export runtime/gherkai_runtime/names.py(曾因「CDK 独立工程、不能 import cli」复刻一份、须两处同步改——组合根共享层抽为平级的产品本体包后复刻消除,ADR 0016「演进」节/0033)。stack 名的推导(names.stack_name)也是单一真源:app.py 建 stack 用它,命令做 VPC 档三态比对时 DescribeStacks 用同一个名。
VPC 三档 + 档比对(踩过的坑,别放松)
--vpc 必给、无隐式默认(deploy / --diff / --synth-only / destroy;--bootstrap 是账户级动作、不合成 stack、不需要它),三档:
| flag | 含义 | SSM vpc 里记成 |
|---|---|---|
--vpc default |
用账户默认 VPC | default |
--vpc new |
本 stack 新建(2-AZ、零 NAT) | new:<所建 vpc-id> |
--vpc vpc-xxxx |
复用现有 VPC | vpc-xxxx |
三档都走公有子网 + assignPublicIp=ENABLED 出网连 AgentCore/Bedrock/S3/DDB(worker 只出不入),零 NAT 成本;真私有隔离(NAT / VPC endpoint)留 backlog,改它要同步 _worker_subnet_ids 与 cli 的 assignPublicIp(跨组件联动)。
⚠️ 为什么必给、还要比对:vpc 的选择不进 stack state。裸 cdk 时代漏带 context 的后果是 diff 里出现「新建整套 VPC + 替换 WorkerSg」的危险变更集——这是真踩过的坑。只强制显式给值挡不住「第二次 deploy 敲错档」,故 stack 把生效档写成 SSM 参数,命令在调 cdk 前比对,三态齐全:
- stack 不存在 = 真首次部署 → 放行;
- 参数缺失但 stack 已存在 = 这个部署早于档登记机制(本机制之前部署的环境都没有这个参数,而这恰是最危险的那一次 deploy)→ 退 2,要求先
--diff核对变更集、再带--allow-vpc-change放行一次;- 参数存在:与
--vpc一致 → 放行;不一致 → 退 2(--allow-vpc-change放行,且会打印两个档让你看清)。
--diff自己不做这个比对——它正是三态让你去跑的那条核对手段,拦住它就无路可走。
用
本包是根 uv workspace 的成员,contributor 一次 uv sync 即装齐。下面的命令从仓库任意位置都可跑(uv run gherkai …);已安装的用户直接 gherkai …。
# 首次在某 account+region 用 CDK:先 bootstrap(= cdk bootstrap aws://<account>/<region>;账户级,不需要 --vpc)
gherkai deploy --bootstrap
# 先看清这次会改什么(尤其网络/IAM)——每次 deploy 前的推荐动作
gherkai deploy --diff --vpc default --prefix gherkai-
# 真部署/更新
gherkai deploy --vpc default --prefix gherkai-
gherkai deploy --vpc new --require-approval broadening # 新建整套 VPC;IAM 变更要人过目
# 逃生舱:只导模板到 DIR、不碰账户(交给自己的审批/发布流水线去 apply)
gherkai deploy --synth-only ./out --vpc default
# 拆栈(RETAIN 语义见下)
gherkai destroy --vpc default --prefix gherkai- # 交互终端下 cdk 会再问一次;脚本/非 TTY 加 --yes
# worker 镜像子命令(见下「worker 镜像」)——deploy 的子动词,不是独立命令
gherkai deploy push-worker acme-novaact:login --engine novaact --variant login
gherkai deploy list-workers
--region / --profile 与 run/submit 同名同义(region 解析链 --region > AWS_REGION > AWS_DEFAULT_REGION > profile config,ADR 0016 决策 C)。退出码:0 成功;2 前置/校验失败(Node 缺失、VPC 档不符、读后端失败、容器引擎名不认、push-worker 的架构/版本 skew 拦截);1 cdk 已成功而 worker 镜像四步失败(账户已被改动,重跑 gherkai deploy 幂等收敛);其余为 cdk CLI 自己的返回码(原样透传)。
--refresh-context 丢弃本机缓存的 CDK 环境查询结果重新查询。--vpc default / --vpc vpc-<id> 走 CDK 的 from_lookup,查到的 VPC/子网/AZ 存在 cdk.context.json;命令的工作目录是一次性的,故把它按 prefix 持久化到 $XDG_CACHE_HOME(缺省 ~/.cache)/gherkai/cdk-context/<prefix>cdk.context.json——这是 CDK 自己的标准做法(它建议入库),也避免每次重查、以及 cdk 对缺失查询值先用占位 VPC 预合成一遍时被 aws-cdk-lib 的模板校验器打出的 [Warning] Template validation found issues…(占位模板的误报;首次查询某 prefix 时仍会出现一次,之后走缓存即消失)。默认 VPC 的子网变了、或那条 warning 之外真出现「新建/替换子网」的变更集时,先 --refresh-context 再 --diff。
--stop-timeout N 标定 worker container 的 SIGTERM→SIGKILL 宽限(默认 120s)。Fargate 硬上限就是 120s,>120 会在部署期被 ECS 拒——命令/synth 期就 fail-fast、点名这是平台限制而非笔误(Nova 的 grace 下限 150s > 120s 这个冲突正卡在这条硬上限上,见 ADR 0032)。
本地验证(不碰 AWS)
uv run pytest deploy_aws/tests -q # stack 合成断言 + Provider + Lambda asset + worker 镜像族(纯本地)
worker 镜像族的测试分两层:tests/test_workers.py 用 moto(SSM/ECS/ECR/DDB)+ 假容器引擎验编排(步序、幂等查重、血缘 tags、清理两道闸);tests/test_container.py 末尾三条用真 docker 验 mock 不出来的引擎事实(本地未推送镜像 RepoDigests 为空、arm64 镜像被架构判据拒),无 docker 时自动 skip。真 ECR push / RunTask 拉起注册出来的 revision 需要真账号,不在单测里。
worker 镜像:基底 / variant / 默认指针(ADR 0038)
CDK 只建 ECR repo 与 模板 task-def revision,镜像与 variant 的注册归 gherkai deploy 族命令。三个概念:
| 概念 | 是什么 | 谁写 |
|---|---|---|
| 基底 | ghcr.io/zhiyanliu/gherkai-worker-<engine>:X.Y.Z,linux/amd64,零使用方内容 |
维护者 CI |
| variant | 一套具名的确定性 step 集 = 一个定制镜像,落 ECR tag <CLI 版本>-<variant>,并对应一个 task-def revision(镜像按 repo@sha256:<digest> 引用) |
部署方 push-worker |
| 默认指针 | 提交时不给 --worker-variant 用哪个 variant(SSM,部署级一个) |
gherkai deploy 初始化为 base;push-worker --set-default 改指 |
gherkai deploy 本身跑四步(全部幂等,重跑收敛):① 登记模板 revision ARN 到 SSM(随 cdk 事务)→ ② 从 GHCR 同步当前版本基底、推成 <版本>-base → ③ 默认指针缺失则初始化为 base(已存在则不动——它记的是团队意图)→ ④ 模板变了就用新模板 + 已记录的 digest 重派生既有 variant,末尾跑一次清理 pass。这一期 deploy 机器需要容器引擎(②要 pull/push);cdk 成功而后三步失败 → 退 1、提示重跑幂等收敛。
定制镜像模板(三行,gherkai 不拥有构建)
FROM ghcr.io/zhiyanliu/gherkai-worker-novaact:1.4.0
COPY steps/ /app/steps
ENV GHERKAI_STEPS_DIR=/app/steps
docker build --platform linux/amd64 -t acme-novaact:login .
⚠️ 必须
--platform linux/amd64:Fargate task-def 固定X86_64(ARM64 是被拒方案);arm Mac 上不加会 build 出 arm64,容器启动期exec format error挂死——错误发生在启动期、不易一眼看出是架构问题。push-worker会在推送前inspect校验架构、不匹配即退 2,把这个坑从 Fargate 启动期提前到推送前。
推送与查看
# 推一个本地镜像成某引擎的一个 variant(一次一个引擎;两个引擎跑两次)
gherkai deploy push-worker acme-novaact:login --engine novaact --variant login --prefix gherkai-
gherkai deploy push-worker acme-midscene:login --engine midscene --variant login
# 顺手把默认指针指过去(该 variant 在另一引擎还没有 → 只警告不拦)
gherkai deploy push-worker acme-novaact:common --engine novaact --variant common --set-default
# 看当前版本有哪些 variant、默认是谁、哪些 revision 待清理
gherkai deploy list-workers
# 提交时选 variant(缺省 = 默认指针)
gherkai submit features/ --backend cloud --worker-variant login
push-worker 做的事(细节与理由见 ADR 0038「push-worker 流程」):版本 skew 前置(CLI 新于后端 → 退 2,无放行口)→ inspect 校验存在与架构 → ECR 登录 → tag + push → 推送后再 inspect 取 digest(本地未推送的镜像没有 registry digest,.Id 是 config digest、注册能过而 RunTask 才 manifest unknown)→ 按(模板 ARN、digest)查重 / 复用孤儿 / 否则从模板注册新 revision(血缘 tags:gherkai:variant|version|digest|template)→ 写 SSM 映射 → 旧 revision 打 gherkai:retired-at + 跑一次清理 pass。
- tag 可变:重推同名 variant 直接放行,只打印「原 digest → 新 digest」。在跑 run 手里的旧 revision 按 digest 指着旧镜像层、不受影响(run 内镜像一致靠 revision,不靠 tag)。
- 清理 pass 是机会式的(挂在
push-worker/deploy末尾,无定时任务):退休或对账出的孤儿 revision,只在退休满 1 小时且无未到终态的 run 引用时才 deregister + delete;否则留到下次。滞留无害。 --container-engine(或 envGHERKAI_CONTAINER_ENGINE;deploy与push-worker都收):这一期只实装docker,别的名字退 2(不静默回落)。delete-worker是留的口子:退 2 并说明押后的是回收策略(旧版本 variant 的 ECR tag / untagged 层),见 ADR 0038 重议闸门。
SSM 里因此多三族参数(都在 /{prefix}backend/* 下):worker-template/<engine>(stack 写)、worker-image/<engine>/<版本>-<variant>(JSON:template_arn / revision_arn / digest / pushed_at,push-worker 写)、worker-default(默认 variant 名)。退休时刻与血缘不进 SSM,以 task-def 的 tags 承载(与 revision 同生死、清理对账只看一处)。
版本升级时的镜像半边(ADR 0037 决策 7「升级即三步」的 ②③)
uv tool upgrade gherkai—— CLI 升到新版本。gherkai deploy—— 新 stack + 新版本base同步进 ECR + 对新版本已有的 variant 重派生(升级当下通常没有)。- 有自定义 variant 的:从新版本基底重新 build 各引擎镜像,
push-worker推上去(tag 含 CLI 版本,故新版本的 variant 是一套新 tag)。
默认指针不重置:若默认是 common 而 1.5.0-common 还没推,提交侧 preflight 会退 2 并提示「push-worker … --variant common,或临时 --worker-variant base」;推上去即恢复,不必再 --set-default。variant 按版本隔离(键含版本),旧版本的留在 ECR/SSM 作历史、不参与当前版本解析。
清理(destroy 之后还要手动删)
数据类资源不随 destroy 删(表/桶/ECR RemovalPolicy.RETAIN、防误删,ADR 0033)——gherkai destroy 之后 2 张 DDB 表 + artifacts 桶 + 2 个 ECR repo 残留、需手动删:
aws dynamodb delete-table --table-name gherkai-runs
aws dynamodb delete-table --table-name gherkai-events
aws s3 rb s3://gherkai-artifacts --force # 桶非空需 --force
aws ecr delete-repository --repository-name gherkai-novaact-worker --force
aws ecr delete-repository --repository-name gherkai-midscene-worker --force
不手动删则同 prefix 重新 deploy 会因资源已存在而冲突。cluster / task-def / SSM 参数(含版本戳与 VPC 档)/ 日志组随 stack 销毁、无需手动——部署戳有意不 RETAIN:它是部署元数据,留着只会让下次 deploy 拿到已消失环境的档。
Metadata
Release files for gherkai-deploy-aws 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gherkai_deploy_aws-1.4.0.tar.gz | 132.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gherkai_deploy_aws-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 223.2 kB
Release files / gherkai_deploy_aws-1.4.0.tar.gz
| Download URL | gherkai_deploy_aws-1.4.0.tar.gz |
|---|---|
| Size | 132.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60baa8daf87068b6d794b87d5bb0f06b808a2d6ecf2b32d8d9f3643b70107f67
|
|
BLAKE2b-256 checksum How to use checksums |
5fd678519d17084f11181132e1bd8d7f0b23d29769df2fde0fff102e01cf4a51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / gherkai_deploy_aws-1.4.0-py3-none-any.whl
| Download URL | gherkai_deploy_aws-1.4.0-py3-none-any.whl |
|---|---|
| Size | 90.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8202b874b91e8f52b1417b39a2e338fd4b788b6c915ed79c331aab7e8da196c5
|
|
BLAKE2b-256 checksum How to use checksums |
43d7cbd9c2a1084a3628b4e484ccfb10b2fd02e5e3e721183cedc75b00e279ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log