报告错误却继续运行的部署脚本,比根本不运行的脚本更糟。客户端配置会触及受保护的密钥、网关处的身份,以及一个可能已经在承载流量的服务。其中每一项都需要停止条件,脚本还必须知道它在停下时走到了哪个阶段。

把整件事当作一笔带有五个检查点的事务,并让每个检查点自行拒绝继续。

在触碰文件系统之前先把输入修好

从一个明确的契约开始:一个身份、一个配置来源、一个预期的服务归属。核实配置存在、它引用的证书和密钥可读,并且目标目录属于预期的软件包。路径能解析并不证明它背后的材料是完整的。

尽早拒绝空变量。一个未设置、会展开成空的 $DEVICE 会把 install -m 400 $KEY /etc/openvpn/client/ 变成指向目录的复制,而故障要到后来才以一个读不到自己密钥的服务现身。

在暂存目录里组装整套配置

先把完整的一套建在私有位置:配置、CA、客户端证书、密钥,以及配置会调用的任何辅助程序。用 install -m 400 -o root -g root 复制,让权限在创建时就定好,而不是之后。然后检查配置内部的引用,因为服务一旦启动,相对的 ca 或 key 行会相对该 unit 的工作目录解析。在整个文件集通过校验之前,不应往活动目录写入任何东西。

记录本次运行所产出内容的非机密指纹,例如在版本标记旁保存的 sha256sum office.conf。正是这一个值让重跑能区分更新与什么都没做。

原子替换,并保留上一套

在文件系统允许的地方,用一次操作把准备好的文件集移到位置,并把已评审的上一套保留在一个带时间戳的名字下。一次部分复制会把新配置留在旧密钥旁边,从而产生一个用错误身份完成认证、且不会记录任何有用原因的服务。如果替换无法做到原子,就把移动排好序,让配置最后落地。

当目标已经存在时,明确判断这次运行是更新、完全相同的重跑,还是与另一台设备的配置冲突。悄悄覆盖属于另一个身份的配置,正是两台机器最终共用一张证书的方式。应该拒绝这次运行,并点名冲突的归属。

只激活一个归属并证明它

只选一条激活路径,systemd 实例或 NetworkManager,并始终如一地使用。在已启用的 unit 旁边启动一个非受管的 openvpn 进程,会得到两条隧道、互相竞争的 DNS 更新,也无法清楚回答停止命令针对的是哪一条。激活之后,先检查 systemctl is-active openvpn-client@office,再检查到远端前缀的路由,然后检查隧道所安装解析器的 resolvectl status。最后以能够访问一个正是隧道存在理由的资源收尾。

一个正在运行的进程和托盘里的绿色图标属于进程事实,并不证明身份与流量策略经受住了交接。

重跑与退役是两条独立的工作流

第二次运行应当收敛到同一批已评审文件与同一个归属,而不是创建第二个账户或第二个守护进程。退役是它自己的流程:停掉那个确切的归属,按策略归档或删除本地配置,并在网关处吊销该身份。删掉本地的密钥文件并不会吊销一份已经离开这台机器的副本。

保留一条不依赖隧道的恢复路径,例如控制台访问或第二条管理路由。一次把你锁在机器外面的部署变更就是失败的变更,哪怕每个阶段都报告成功。

值得保留的脚本,是围绕归属与证据的一笔事务。一串简短的提权复制命令并不是同一回事。

相关文章