一个 webhook 触发的自动部署流水线
curl -X POST https://example.com/deploy -H "X-Signature: ***",很多自动部署的起点,其实就这一行。
最近有读者问,别光讲概念,能不能直接给一个能落地的办法。那就直接上:代码仓库有新提交时,服务器收到一个 webhook,请求通过校验后,拉代码、构建、切换版本,失败就停下,别把线上一起带走。
先说人话:这套东西在干嘛
- webhook:别的系统主动打给你一个 HTTP 请求,通知“有事发生了”。
- CI/CD:CI 是自动检查和构建,CD 是自动部署上线。
- 部署流水线:把几个步骤按顺序串起来执行,不靠手点,不靠记忆。
对普通使用者来说,这套东西最大的价值不是“高级”,而是少出错。手动部署最容易翻车的地方,不是命令不会写,而是漏掉一步、顺序跑反、或者把线上目录直接改脏。
最小可用版本:四步就够
一个够用的顺序通常是这样:
- 校验 webhook 签名
- 拉取指定版本代码
- 构建到新目录
- 原子切换到新版本
“原子切换”可以理解成:不要直接在正在运行的目录里改文件,而是先准备好一个完整的新版本,再把 current 这个软链接切过去。这样要么旧版本继续跑,要么新版本完整接管,中间态最少。
目录可以长这样:
bash
/app
/releases
/20260328-070000
/20260328-081500
/shared
.env
uploads
current -> /app/releases/20260328-081500
关键不是目录多漂亮,而是可回滚。只要旧目录还在,切回去就是改一个链接。
webhook 不难,难的是别裸奔
很多“能跑”的 webhook,其实只是“碰巧还没出事”。最常见的坑有三个。
1. 只要收到 POST 就执行
这基本等于公网谁都能帮你部署。至少要校验签名,确认请求确实来自代码平台。
python
import hmac
import hashlib
def verify_signature(secret: str, body: bytes, header_sig: str) -> bool:
digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
expected = f”sha256={digest}”
return hmac.compare_digest(expected, header_sig)
2. 直接 git pull
git pull 看起来省事,实际很容易把工作区拉脏。更稳一点的做法是新建发布目录,然后拉指定 commit。
bash
set -euo pipefail
APP_DIR=/app
RELEASE_ID=$(date +%Y%m%d-%H%M%S)
RELEASE_DIR=”$APP_DIR/releases/$RELEASE_ID”
git clone –depth 1 https://example.com/repo.git “$RELEASE_DIR”
cd “$RELEASE_DIR”
git fetch –depth 1 origin “$COMMIT_SHA”
git checkout “$COMMIT_SHA”
这样做的好处是,线上目录始终是干净的。坏了就删这个新目录,不污染当前版本。
3. 构建成功前就覆盖线上
别这么干。先在发布目录里构建,成功了再切。
bash
cd “$RELEASE_DIR”
ln -sfn /app/shared/.env .env
npm ci
npm run build
ln -sfn “$RELEASE_DIR” /app/current
systemctl restart myapp.service
这里的 .env 和上传目录这种长期数据,放在 shared。代码版本换来换去,配置和数据不要跟着一起乱飞。
一条能用的部署脚本,重点不是长,是停得住
脚本最好满足三件事:
- 任一步失败立刻退出
- 打日志
- 不并发执行
不并发很重要。两个 webhook 同时进来,两个部署脚本一起跑,最后切到哪个版本,全看运气。
可以用 flock 这种锁:
bash
flock -n /tmp/deploy.lock /usr/local/bin/deploy.sh
拿不到锁就直接返回“已有部署进行中”。这比两个部署同时抢线上目录靠谱得多。
真正常见的不是“部署失败”,而是“部署成功但服务坏了”
所以流水线里最好加一个最小检查。不是为了追求完美,而是为了挡住明显事故。
比如服务重启后,等 3 秒,请求一下健康检查地址:
bash
sleep 3
curl -fsS http://127.0.0.1:3000/health
如果这个请求失败,就不要当作部署完成。更进一步一点,可以自动回滚:
bash
PREV_TARGET=$(readlink -f /app/current)
ln -sfn “$RELEASE_DIR” /app/current
systemctl restart myapp.service
if ! curl -fsS http://127.0.0.1:3000/health; then
ln -sfn “$PREV_TARGET” /app/current
systemctl restart myapp.service
exit 1
fi
这不等于百分百安全,但已经比“脚本跑完就当成功”强很多。至于健康检查该测到什么深度,要看服务本身,不能一概而论;如果没有验证过,就先从最小可用检查开始。
哪些东西不该放进自动部署
这类系统的边界要提前讲清楚:
- 密钥不要写进仓库
- 不要让 webhook 直接执行任意命令
- 不要把构建日志原样公开
- 不要把服务器信息、IP、目录、Token 暴露在通知里
通知可以发,但内容要克制一点:
- 成功:版本号、时间、耗时
- 失败:步骤名、错误摘要
- 敏感字段全部打码
图省事把内部信息一起吐出去,后面通常会更麻烦。
一个更实际的取舍
如果网站或服务不复杂,没必要一上来就堆一整套很重的平台。一个 webhook 接一个脚本,再加日志、锁、回滚,已经能解决很多个人项目和小型服务的部署问题。
工具越多,排障路径越长。真正实用的流水线,往往不是功能最多的,而是你过几周再看,出问题还能很快定位的那种。
比较稳的做法通常就这几条:
- webhook 只负责触发
- 部署脚本只做部署
- 配置和数据独立
- 切换版本必须可回滚
- 每一步都能单独手动重跑
再往前走一步,更值得补的反而不是“再接几个通知渠道”,而是:数据库迁移要不要纳入同一条流水线,以及失败时怎样保证它不会把线上数据一起拖下去。