一个 webhook 触发的自动部署流水线
curl -X POST https://<WEBHOOK_HOST>/hooks/deploy -H "X-Signature: <SIGNATURE>" 可以作为自动部署的触发入口。
这篇只讲一个最小可用方案:代码仓库推送后,向自己的机器发送 webhook;机器验证签名;通过后执行部署脚本;脚本拉代码、构建、切换版本、做一次健康检查。重点是能跑、能回滚、重复执行不容易出问题。
流水线拆开看
这里的 webhook,可以简单理解成“外部系统一有动作,就向你发一个 HTTP 请求”。
最小链路只有四步:
- 代码仓库收到 push
- 仓库向 webhook 地址发请求
- 服务器验证请求确实来自仓库
- 服务器执行部署脚本
CI/CD 这个词很大,放到这里可以只理解成两段:
- CI:有人提交代码后,自动做检查或构建
- CD:检查通过后,自动部署到目标机器
如果只是个人项目或者低频更新,可以先把“检查”和“部署”分开,先打通部署链路。
webhook 服务别写复杂
如果已经有反向代理,挂一个很小的服务就够了。下面用 Node.js 做示例,重点只有两件事:验签、落日志。
一个最小的 webhook 接收器
js
import express from “express”;
import crypto from “crypto”;
import { execFile } from “child_process”;
import fs from “fs”;
const app = express();
const SECRET = process.env.WEBHOOK_SECRET || “
app.use(express.raw({ type: “/“ }));
function verifySignature(req) {
const sig = req.header(“X-Hub-Signature-256”) || “”;
const hmac =
“sha256=” +
crypto.createHmac(“sha256”, SECRET).update(req.body).digest(“hex”);
const sigBuf = Buffer.from(sig);
const hmacBuf = Buffer.from(hmac);
if (sigBuf.length !== hmacBuf.length) {
return false;
}
return crypto.timingSafeEqual(sigBuf, hmacBuf);
}
app.post(“/hooks/deploy”, (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send(“bad signature”);
}
fs.appendFileSync(
“/var/log/deploy-hook.log”,
${new Date().toISOString()} deploy\n
);
execFile(“/bin/bash”, [“/srv/app/deploy.sh”], (err, stdout, stderr) => {
fs.appendFileSync(
“/var/log/deploy-hook.log”,
(stdout || “”) + (stderr || “”) + (err ? \n${err.message}\n : “”)
);
});
res.send(“accepted”);
});
app.listen(9000);
这段代码里有几个点不能省:
- 密钥只放环境变量,不写死在仓库
- 验签必须做,否则任何人都能请求部署接口
- 收到请求先快速返回,不要让 webhook 长时间等待
- 日志单独落盘,出错时才知道卡在哪
部署脚本要可重复执行
部署脚本最怕第一次成功,第二次出问题。幂等可以简单理解为:同一个脚本跑两次,结果尽量一致。
一个偏保守的 deploy.sh
1 | |
APP_DIR=”/srv/app”
REPO_DIR=”$APP_DIR/repo”
CURRENT_LINK=”$APP_DIR/current”
RELEASES_DIR=”$APP_DIR/releases”
STAMP=”$(date +%Y%m%d%H%M%S)”
NEW_RELEASE=”$RELEASES_DIR/$STAMP”
mkdir -p “$REPO_DIR” “$RELEASES_DIR”
if [ ! -d “$REPO_DIR/.git” ]; then
git clone
fi
cd “$REPO_DIR”
git fetch –all
git reset –hard origin/main
mkdir -p “$NEW_RELEASE”
rsync -a –delete –exclude “.git” “$REPO_DIR/“ “$NEW_RELEASE/“
cd “$NEW_RELEASE”
npm ci
npm run build
ln -sfn “$NEW_RELEASE” “$CURRENT_LINK”
systemctl restart app.service
sleep 2
curl -fsS
这份脚本刻意用了几个保守做法:
git reset --hard origin/main:每次都回到远端最新状态,避免本地脏文件影响部署rsync到新目录:不要在运行中的目录直接覆盖ln -sfn切软链接:切换版本接近瞬时,回滚也简单- 健康检查失败就退出:部署不是“服务重启了”就算完成
如果要回滚,也很直接:把 current 链接指回上一个 release,再重启服务。这个方案不复杂,出问题时也容易手动处理。
这几处最容易踩坑
1. webhook 收到太多次
一个 push 可能触发多类事件,或者短时间连续推送。最省事的办法是加锁,防止并发部署。
1 | |
拿不到锁就直接退出,比两次部署互相覆盖安全。
2. “成功”只是脚本执行完
真正要看的不是命令返回 0,而是服务能不能对外响应。健康检查地址最好单独做;如果没有,也至少检查进程和端口。关于检查哪些依赖、返回哪些状态,本文未验证统一做法。
3. 日志太少
部署至少要记三类日志:
- webhook 是否收到
- 脚本执行到哪一步
- 健康检查是否通过
不要只留一行 deploy success。
用现成 CI,还是自己接 webhook
如果已经在用 GitHub Actions、GitLab CI 这类平台,也可以让它们通过 SSH 直接部署。自己接 webhook 的价值通常在这几种场景:
- 想少一层平台配置
- 机器在内网,通过反向代理暴露一个入口
- 部署逻辑很本地化,脚本比平台配置更直观
- 想完全掌握日志和回滚方式
代价也明确:安全、重试、并发控制、审计,很多事都要自己补。
一个够用的边界
如果这是公开服务,建议至少再加三道限制:
- 反向代理只放行
POST /hooks/deploy - 限制来源地址;如果来源列表会变,先标注为“待确认”,不要写死未经验证的范围
- webhook 进程和应用进程分离,别共用过高权限
另外,不要让 webhook 直接执行任意命令。固定调用一个脚本,比把参数从请求体透传进去安全得多。
下一步可以补什么
上面这套只解决了“触发后自动部署”。如果要继续完善,通常会补上测试、制品打包、灰度发布和自动回滚。先补哪一步,取决于当前最常见的失败点。