qianwenyun-deploy 架构拆解:Agent 当大脑、脚本当手脚的部署技能
它到底是什么
qianwenyun-deploy 不是一个应用,没有构建、没有测试、没有运行时。它是一个 Claude Code 技能包(skill),装进 Claude Code 之后,Agent 就能用自然语言一句话把一个项目部署到阿里云(千问云)上,底层走的是 ROS(资源编排服务)。
仓库是少见的双层嵌套布局:外层 qianwenyun-deploy/ 包着一个同名的内层目录,真正会被当作技能安装的是内层那个,里面有 SKILL.md、scripts/ 和 reference/。SKILL.md 是整个技能的入口,把部署流程写成 14 步全栈部署和 3 步热更新;reference/ 是 Agent 在每一步做决策前要回头查的规则手册。
技能支持两种交付形态:单机(1 台 ECS + EIP + VPC + 安全组,公网 IP 直连)和高可用(2 台跨可用区 ECS + SLB + VPC + 安全组,SLB 做入口),需要数据库时再加一台 RDS MySQL 8.0(走内网)。全栈按量付费(PostPaid),交付一个公网入口。
一张图看懂
下面这张图把整个技能的架构按「控制面 → 数据面」铺开了,暗亮主题可切换,也能导出 PNG/SVG。
图里从左到右是一条主路径:用户把意图用自然语言丢给 Agent,Agent 调脚本、脚本薄封装 aliyun CLI、CLI 调 ROS、ROS 编排出云上资源。OSS 临时桶和 .qianwenyun-deploy 状态文件是从主路径分出去的两个侧支——一个搬运构建产物,一个记录部署状态供热更新回滚。
核心设计 当大脑,脚本刻意保持笨
整个技能最反直觉、也最值得讲的一点,写在 CLAUDE.md 里一句话:scripts only collect signals or call APIs; the Agent makes all decisions。
scripts/analyze_project.py 的文档字符串把这个原则钉得很死:它「不做决策」,不输出 app_type、backend_entry、backend_port 这些判断结论,只采集 file_tree、config_files、source_samples、db_signals、env_files 这些原始事实,把 JSON 吐到 stdout。真正去判断这是个 Flask 还是 Go 二进制、Nginx 该用 proxy 还是 static-proxy、后端入口命令长什么样的,是 Agent 自己,依据是 reference/project_type_guide.md。
这条线划得很硬,因为错一个 nginx_mode 是静默故障static-proxy 而不是 proxy,try_files 会把动态路由直接吃掉,页面 404 但栈状态是 CREATE_COMPLETE。脚本不会替你兜这个底,所以判断必须留在 Agent 这一侧,拿不准就走 AskUserQuestion 问用户,而不是猜。
Shell 脚本同样薄。check_env.sh、check_stock.sh、create_stack.sh、wait_stack.sh、delete_stack.sh 这些,本质都是对 aliyun CLI 一次调用的封装,加上一组环境变量契约和退出码契约。退出码是 Agent 分派的依据:check_env.sh 返回 0 是通过、2 是 CLI 没装、3 是凭证无效、4 是没配 region、5 是缺子命令、6 是 AK 身份探测失败;wait_stack.sh 返回 0 是 CREATE_COMPLETE、2 是任意 FAILED/ROLLBACK、3 是超时。Agent 读这些码决定下一步走哪,脚本自己不分支。
三条入口路由
技能一上来先路由,信号不同走不同流程:
| 信号 | 工作流 |
|---|---|
已存在 .qianwenyun-deploy 且用户说「更新」 | 热更新(U1–U3) |
消息里带 Git URL(github/gitlab/gitee 或 .git 后缀) | 全栈部署,步骤 2 先 clone |
| 其它 | 全栈部署,从本地项目起 |
全栈部署 14 步,热更新 3 步加可选回滚。部分步骤会按信号条件跳过:没有 Git URL 就跳过步骤 2,没有数据库信号就跳过步骤 5。SKILL.md 要求无论跳不跳,都要把完整步骤列表渲染出来,跳过的步骤标注原因。
控制面走一遍:从一句话到 ROS 栈
把 14 步压缩一下,能看出决策点都在哪。
步骤 1 check_env.sh 先验环境,凭证无效就引导用户在另一个终端跑 aliyun configure——技能明确禁止在聊天里收集 AK/SK。步骤 3 analyze_project.py 出信号,Agent 定 app_type、backend_entry、backend_port、nginx_mode,顺带扫一遍有没有硬编码的密钥/Token,发现就警告。
到步骤 4 check_existing.sh 按 from=qianwenyun 这个 tag 扫 ROS 栈,发现同项目已部署就停下来问用户:热更新(推荐,IP 不变)还是删了重部。步骤 5 看 db_signals,只有 MySQL 信号才给建 RDS 的选项,postgres/redis 之类直接告知 v1 不支持,这是写死的范围限制,不是配置开关。
步骤 6 选拓扑和规格。ECS 给了三档预设:入门型 ecs.e-c1m1.large 约 ¥0.10/时、通用型 ecs.e-c1m2.large 约 ¥0.31/时、性能型 ecs.e-c1m2.xlarge 约 ¥0.62/时。RDS 同样三档:mysql.n2e.small.1、mysql.n2.medium.1、mysql.n4.medium.1。这些规格 ID 和单价都写在 SKILL.md 里,不是从云上动态拉的。
步骤 8 check_stock.sh 决定可用区,这是硬约束——可用区不能手填,必须从库存查询来。含 RDS 时必须传 DB_INSTANCE_CLASS,让脚本算 ECS 和 RDS 都有货的可用区交集,否则选出来的区可能 RDS 下不了单。步骤 9 先建一个临时 OSS 桶把模板传上去,跑 validate_template.sh 和 estimate_cost.sh,ROS 必须用 --TemplateURL,--TemplateBody 会被 WAF 拦。询价完汇总确认时同时给小时价和月价(小时价 ×730),让短时试用和长期运行两种场景都能对得上。
步骤 11 create_stack.sh 创建栈,带 from=qianwenyun、qianwenyun-appName、qianwenyun-appDesc 三个 tag,DisableRollback=false 保证失败自动回滚。拿到 StackId 后立刻写一个 provisional: true 的最小状态文件,这样部署中途被打断也还能清理——之后 record_state.py 会用完整状态覆盖它。
数据面 编排的云资源
ROS 这一层负责按 reference/ros_template_{single,ha}[_rds].yaml 这些模板把资源按依赖顺序建出来。generate_template.py 从模板渲染出 ROS 模板和 userdata 脚本,--artifacts-json 这个管道把 upload_artifacts.py 产出的签名 URL 直接灌进模板,不靠手复制——签名 URL 手动粘一旦拼错,ECS 引导时拉不到产物,问题非常难查。
ECS 上的引导全靠 userdata。reference/userdata_nginx_proxy.sh、userdata_nginx_static.sh、userdata_nginx_static_proxy.sh、userdata_systemd.sh、userdata_docker.sh 这几套按 nginx_mode 和 app_type 选用,负责起 Nginx、拉后端产物、起进程。OSS 临时桶带 7 天生命周期,签名 URL 24 小时有效,upload_artifacts.py 把前端 dist 和后端镜像/二进制打包传上去,ECS 引导时拉下来。
步骤 13 探活是两道关:先 curl /healthz 重试 12 次证明 Nginx 活着,有后端的话再 curl / 看状态码。502/504 在这里是个关键信号——它意味着 Nginx 起来了但后端没起,栈状态是绿的,应用是死的,Nginx 把故障盖住了。两关都过才算成,失败就用 Cloud Assistant 的 RunCommand 上去查 /var/log/qianwenyun-bootstrap.log 和 /var/log/qianwenyun-app.log。
热更新:不动云,只换代码
热更新的前提是项目根目录已有 .qianwenyun-deploy。它不删栈不重建资源,公网 IP 不变,目标 3 分钟内完成。
U1 用 upload_artifacts.py 把新产物传到首次部署复用的那个 OSS 桶(桶名从状态文件读)。U2 update_app.sh 通过 Cloud Assistant 的 RunCommand 在 ECS 上跑命令,顺序是:拉新产物到暂存区 → 预装依赖 → 停服务 → 原子替换 → 重启,把停机窗口压到最小。HA 模式下自动逐台滚动,不会两台一起断。U3 复用步骤 13 的两道关探活,脚本顺手写回 updated_at 和 previous_artifact_urls。
回滚就是 ROLLBACK=1 bash scripts/update_app.sh,从状态文件读上一版本 URL 重新下发。但签名 URL 只有 24 小时有效,过期了得重新上传。
几条硬约束
这套技能把边界写得很明确,不给自己留余地:
- AK/SK、token、密码一律不在聊天里收,凭证问题一律引导去独立终端跑
aliyun configure,也不建议用!前缀这种取巧方式。 - ROS 必须
--TemplateURL,--TemplateBody会被 WAF 拦;ValidateTemplate不能跳。 - 可用区必须来自
check_stock.sh,不能手填;含 RDS 必须传DB_INSTANCE_CLASS取交集。 - 密码(ECS 和 RDS 各一套,不复用)至少 12 位,特殊字符限定在
!@%^*+=_-,因为& # $ | ;这些会破坏db.env的 sourcing;密码由 Agent 生成、写进.qianwenyun-deploy.local(0600),不打印到聊天。 - 产物 URL 必须是 OSS 内网 endpoint(HA 的 ECS 没公网 IP),且必须经
--artifacts-json管道传,不能手粘。 - RDS 只支持 MySQL 8.0,单可用区,不碰 PG/Redis/MongoDB。
- 清理只能走
delete_stack.sh,ROS 按依赖顺序释放资源;严禁手动逐个删 ECS/VPC/EIP/安全组,那样会导致栈状态不一致。DELETE_FAILED时用aliyun ros ListStackResources查哪个资源卡住,处理后再重试 DeleteStack,不能手动删完重试。
这些约束不是防御性编程,是技能 v1 的范围声明:只做按量付费、单地域、无 HTTPS、MySQL-8.0-only、24 小时签名 URL 回滚窗口,其余的不开配置项、不预留分支。
参考资料
- 架构图 qianwenyun-deploy-architecture.html — 本文配套架构图,暗亮主题可切换,可导出 PNG/JPEG/SVG
- 什么是资源编排服务 ROS — ROS 模板与资源栈的自动化编排定义,对应正文数据面编排部分
- 阿里云 CLI 命令行工具 —
check_env.sh校验的 aliyun CLI 安装与 profile 配置 - ECS 云助手 创建/执行命令(RunCommand) —
update_app.sh热更新与回滚依赖的 RunCommand 通道