文档/ 部署/ 部署钩子

部署钩子

在 Git 部署的四个时间点,从应用镜像中运行一条命令:构建前后、流量切换前后。

什么是部署钩子

部署钩子是挂在某个应用及其部署某一阶段上的单条命令。sh0 在由该应用镜像启动的一次性容器中运行它,将输出记录到部署日志中,并根据退出码决定部署是否继续。

典型用途:在流量到达新版本之前执行数据库迁移(npm run migrate、php artisan migrate --force),对刚构建的镜像做冒烟测试,切换后预热缓存。

哪些应用会运行钩子

只有从 Git 仓库部署的应用才会运行钩子。对于其他任何应用(通过 sh0 push 上传归档、Docker 镜像、粘贴的 Dockerfile、Compose 文件、模板),sh0 会拒绝创建钩子,因为钩子永远不会被触发:

Terminal
$ 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路由切换并移除旧容器之后新镜像,带有环境变量和卷。记录为失败;部署仍视为成功(新版本已经上线)。
Warning
钩子没有单独的“出错即失败”设置,完全由阶段决定: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):

Terminal
$ 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      Yes

sh0 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 钩子会显示沙箱用户:

Build log
[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 行,其中的密钥会被遮蔽。以下是一次带五个钩子的首次部署的真实日志:

Build log
[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 结束,已上线的版本继续运行:

Build log
[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.

这种情况下,失败钩子以及同阶段在它之前运行的钩子的输出都不会保留,只剩下错误行。

限制

  • 含有禁用字符的命令在创建钩子时会被接受,只在运行时才被拒绝。在阻断型阶段,部署会因此失败;在 post_deploy 阶段,该钩子会从日志中消失,不留痕迹。保存前请检查命令。
  • 钩子不发送通知。sh0 的告警及其渠道(邮件、Slack、Discord、Telegram、webhook)见监控页面。
  • Git 应用的回滚会重新走完整个流水线,包括钩子。回滚实际部署的内容见回滚页面。