这篇文章讲的不是某个框架,而是把"部署"从一个手工动作变成一条可验证、可回滚、能自证的流水线。起因是一次很典型的灵异事件:我改完页面、传上去、自己 curl 验证通过,两个小时后朋友说"你那个站点还是旧版"。查了半天才发现——那个站点根目录根本不是我手工维护的目录,而是某个定时任务的输出目录。它每 6 小时重建一次,然后整目录覆盖过去。
想清楚之后,做法很简单也很粗暴:
这条规矩的价值不在于"规范",而在于它让"线上内容从哪来"这个问题有了唯一答案。
人工部署和定时任务共用同一个入口脚本 run.sh,串行执行五步,任何一步失败就中止:
# run.sh —— 唯一发布入口,五步串行 1. flock 互斥 并发触发时第二个实例直接退出(退出码 75) 2. 前置断言 路径/磁盘余量不够就直接失败,不产出半成品 3. 构建 + 产物门禁 产物标记缺一项就拒绝发布 4. 发布前快照 打包当前线上版本,保留最近 N 份 5. rsync --delete 同步 → 修属主 → 关键路由 smoke # 全程 trap ERR 写部署日志 # 失败必须留痕,不能只留一个非零退出码
两个细节值得单独说。互斥锁解决的是"定时任务和我手动跑撞在一起"——两个进程同时往同一个目录 rsync,结果不是"后一个覆盖前一个"这么温和,而是可能留下一个混合了两个构建的目录。前置断言解决的是"盘满了才失败"——构建到一半发现写不进去,站点已经处于半成品状态,那比一开始就拒绝执行糟糕得多。
门禁脚本不是单元测试,它检查的不是逻辑正确性,而是产物完整性:该生成的页面在不在、页面里该有的关键内容在不在、资源指纹是否已经更新、站内链接指向的产物是否齐全。
这套检查从 27 项长到 36 项的过程,其实就是把"每次发布前我脑子里会过一遍的清单"一条条搬出来交给机器。它的直接好处是:我再也不需要在上线前靠记忆做检查,而且新增内容时不会忘记同步更新检查项——因为门禁不过,发布根本不会发生。
每次构建都会产出一份 build-manifest.json:构建号、页面数、资源指纹,以及每一个产物的 SHA-256。同时把构建号写进页面的 head 里。
后一个动作看起来多余,但它解决了一个很实际的问题:打开任意一个页面,不用登录服务器就知道线上跑的是哪个构建。以前的排查方式是"FTP 上去看看文件时间戳",现在是一行 grep。
有了 manifest,就能反过来干一件更有价值的事:拿它和线上站点根逐文件比对,输出三类差异——缺失、被改、多出来的文件。
$ python3 verify_manifest.py MISSING 0 MODIFIED 0 EXTRA 0 NO DRIFT (456 files)
实测一次线上 456 个文件全部一致。但真正体现价值的是不一致的时候:曾经有人在线上直接改了一个文件,第二天漂移检测就把它标成了 MODIFIED。手改不是被禁止,而是必须可见——这条比"禁止手改"现实得多,因为紧急情况下你总会想直接动线上,那就至少让这件事留下记录。
接入站是个前端应用(SPA),它自带的文档指向了别处。我想要一套"和接入站同一套配色与组件语言"的文档,但不想在应用自己的构建里再塞一套文档工具链——那意味着两套依赖、两个构建、两次可能失败。
最后的做法是让文档站由主站流水线生成,产物落在主站站点根的一个子目录里,再在服务器上用一段静态直出的路由把它暴露出去。这个方案有几个我很喜欢的点:
^~ 的前缀路由:它的优先级高于兜底路由,所以文档站既不会走到应用的上游代理,也不会被应用的启动骨架注入。x/index.html)而不是 x.html:直接绕开了"重写规则 + 别名"组合下的路径拼接歧义,也不用为它额外写 rewrite。门禁过 + 真实浏览器端到端过,两个都绿才算这次发布成功。中间还有几条是踩出来的:
这套东西加起来也就几个脚本,但它把一件以前靠手感的事变成了可复述的流程。收益可以用"现在我随时能回答三个问题"来概括:
装流水线之前,这三个问题一个都答不上来。现在它们各自对应一条命令。我觉得这就是"工程化"最朴素的定义:不是用了多少工具,而是原来答不上来的问题,现在能答了。
← 返回博客列表