部署钩子
在 Git 部署的四个时间点,从应用镜像中运行一条命令:构建前后、流量切换前后。
什么是部署钩子
部署钩子是挂在某个应用及其部署某一阶段上的单条命令。sh0 在由该应用镜像启动的一次性容器中运行它,将输出记录到部署日志中,并根据退出码决定部署是否继续。
典型用途:在流量到达新版本之前执行数据库迁移(npm run migrate、php artisan migrate --force),对刚构建的镜像做冒烟测试,切换后预热缓存。
哪些应用会运行钩子
只有从 Git 仓库部署的应用才会运行钩子。对于其他任何应用(通过 sh0 push 上传归档、Docker 镜像、粘贴的 Dockerfile、Compose 文件、模板),sh0 会拒绝创建钩子,因为钩子永远不会被触发:
$ sh0 hooks my-site create --hook-type pre_build --command "node --version"
Error: Deploy hooks only run on git-based deploys; app 'my-site' is a 'upload' app, where hooks would never fire. Remove the hook or convert the app to a git deploy. (BAD_REQUEST)如果 Git 应用的仓库以 Compose 栈的方式部署,同样不会运行钩子:Compose 路径在第一个钩子阶段之前就离开了部署流水线。
四个阶段
阶段决定了钩子何时运行、在哪个镜像中运行、能拿到什么,以及失败时的后果:
| 阶段 | 时机 | 镜像与输入 | 失败时 |
|---|---|---|---|
| pre_build | 克隆之后、构建镜像之前 | 当前已部署版本的镜像。没有环境变量,也没有卷。首次部署时跳过(尚无镜像)。 | 部署失败。旧版本继续提供服务。 |
| post_build | 镜像构建之后、新容器启动之前 | 新镜像,带有应用的环境变量。没有卷。 | 部署失败。旧版本继续提供服务。 |
| pre_deploy | 新容器通过健康检查之后、路由切换到它之前 | 新镜像,带有环境变量,并挂载应用的卷。 | 部署失败,新容器被移除,旧版本继续提供服务。 |
| post_deploy | 路由切换并移除旧容器之后 | 新镜像,带有环境变量和卷。 | 记录为失败;部署仍视为成功(新版本已经上线)。 |
pre_build、post_build 或 pre_deploy 钩子失败一定会导致部署失败,post_deploy 钩子失败则永远不会。创建钩子
在控制面板中打开应用,进入 Hooks 选项卡,点击 Create Hook。选择 Hook Type,填写 Command,需要时填写 Timeout (seconds)(默认 300)。
在命令行中,用 --hook-type 指定类型(pre_build、post_build、pre_deploy、post_deploy):
$ sh0 hooks my-api create --hook-type post_build --command "id"
Deploy hook created: 949d4097-4350-48c0-9c7c-6ca3c582a51a
Type: post_build
Command: id
Timeout: 300s
$ sh0 hooks my-api list
ID Type Command Timeout Enabled
----------------------------------------------------------------------------------------------------------
281e3d14-fdca-4478-8ae3-985d4a604562 post_deploy env | grep NODE 300 Yes
1b025181-05a1-4f8e-bd6b-3a26fcb69848 post_deploy printenv PORT 300 Yes
22ee2ab3-7fc4-4714-a05d-f1dd779514f3 pre_deploy echo second 300 Yes
6f1fa1be-b188-4cc0-b277-cf5acfbe0780 pre_deploy echo first 300 Yes
949d4097-4350-48c0-9c7c-6ca3c582a51a post_build id 300 Yes
b8303249-54fe-4358-90f8-a287390c219f pre_build node --version 300 Yessh0 hooks my-api delete <id> 删除钩子。若只想暂停而不删除,发送 PATCH /api/v1/deploy-hooks/<id>,请求体为 enabled=false。API 还提供 GET 和 POST /api/v1/apps/<app-id>/deploy-hooks。创建或修改钩子需要该应用的开发者角色。
钩子在哪里运行
从不在服务器本身上运行。每个钩子都会得到一个一次性容器,由上表所列的镜像启动,具有以下特性:
- 以非特权用户
1000:1000运行,绝不以 root 运行。 - 镜像的
ENTRYPOINT会被清空:命令按原样执行,而不是作为node或npm的参数。 - 它连接到
sh0-net网络,因此可以通过内部主机名访问应用的数据库。 - 不经过 shell。命令按空格拆分后直接执行。
|、;、&、>、<、反引号和$(都会被拒绝,引号按字面传递。凡是需要 shell 的操作,请在镜像中放一个脚本并调用它:sh scripts/migrate.sh。完整规则与定时任务相同;命令长度上限为 4096 个字符。
在 Node.js 镜像上运行 id 的 post_build 钩子会显示沙箱用户:
[HOOKS] post_build hooks:
[OK] id (242ms, exit: Some(0))
stdout: uid=1000(node) gid=1000(node) groups=1000(node)顺序与超时
同一阶段的钩子依次运行,最近创建的最先运行。如果顺序重要,请先创建需要最后运行的钩子,或把各步骤串在一个脚本里。
每个钩子都有自己的超时时间,默认 300 秒。超时的钩子计为失败(日志中显示 Hook timed out after 5s)。它的容器不会被停止:会一直运行到命令自行结束,之后仍留在服务器上。
查看结果
结果会追加到部署的构建日志中,位于 Deployments 选项卡。每个钩子一行,包含状态(OK、FAIL 或 SKIP)、耗时和退出码,随后是标准输出和标准错误的前 10 行,其中的密钥会被遮蔽。以下是一次带五个钩子的首次部署的真实日志:
[HOOKS] pre_build hooks:
[SKIP] node --version (0ms, exit: None)
stderr: skipped: no app image available yet (hooks run in a container, never on the host)
[HOOKS] post_build hooks:
[OK] id (292ms, exit: Some(0))
stdout: uid=1000(node) gid=1000(node) groups=1000(node)
[HOOKS] pre_deploy hooks:
[OK] echo second (287ms, exit: Some(0))
stdout: second
[OK] echo first (295ms, exit: Some(0))
stdout: first
[HOOKS] post_deploy hooks:
[OK] printenv PORT (249ms, exit: Some(0))
stdout: 3001当阻断型钩子失败时,日志以错误信息和新容器的清理结束。下例中 pre_deploy 钩子以退出码 3 结束,已上线的版本继续运行:
[STEP 5/6] Health check passed (previous step: 8.3s)
[ERROR] Deploy hook 'node -e process.exit(3)' failed with exit code Some(3):
[WARNING] Removed the container this failed deployment had started (/sh0-17b83e1c-my-api-6abc39e3). A failed deploy leaves nothing running.这种情况下,失败钩子以及同阶段在它之前运行的钩子的输出都不会保留,只剩下错误行。