↓ 跳过正文

NixOS(十二):更新与多台电脑的部署

·3321 字·7 分钟
锦李本鲤
作者
锦李本鲤
光锥之内,皆为命运。
NixOS系列 - 这篇文章属于一个选集。
§ 12: 本文

缘起
#

前面的文章介绍了配置、机密、数据和服务。电脑多了之后,维护时还需要知道每台电脑更新到了哪一步:仓库是不是同一个版本,系统有没有构建和激活,以及是否还在运行旧内核。

实际维护中,我把常规系统更新整理成了手动流程:在一个地方检查输入更新,再逐台构建、应用和验证。容器与应用仓库的同步也单独记录,不把一次Git更新直接写成系统已经升级成功。

这里介绍这套流程中可以通用的部分。示例假设已有正常使用的flake和Git仓库,使用虚构输出laptop;提交号、路径和主机名称都需要换成自己的,不展示现有电脑的配置。

更新内容分类
#

先区分这次改的是什么:

修改需要完成的工作
普通NixOS配置检查、构建相应主机并应用
flake输入检查锁文件变化及影响范围,再分批部署
NixOS发行分支另外阅读升级说明,确认模块和状态兼容性
容器镜像或应用配置准备运行配置,更新相应服务并验证
文档或操作记录同步Git,一般不产生新系统代际

这些电脑上的容器仍由Compose管理,所以容器更新与NixOS更新不是同一个操作。即使代码已经同步到所有电脑,也可能只有部署主机重新创建了容器,其他电脑只是保留了同一份源文件。

同一个锁文件也不表示每台电脑的系统结果完全相同。它们共用输入,但各自选择不同的主机模块、硬件配置和软件;应分别构建对应输出。

更新前检查
#

检查当前状态
#

在配置仓库中,先查看修改和系统状态:

1
2
3
4
5
6
7
8
9
git status --short
git rev-parse HEAD
readlink -f /nix/var/nix/profiles/system
readlink -f /run/current-system
readlink -f /run/booted-system
uname -r
systemctl --failed
systemctl --user --failed
df -h / /boot

已有修改需要保存和整理,不要为了同步方便直接丢弃。还要记下原本已经失败的服务,避免将旧问题当作这次更新造成的回归。用户服务检查应在对应用户的会话中执行。

Git提交、系统profile、当前激活配置和本次启动配置,是不同的记录。实际维护时遇到过仓库已经更新、系统也已切换,但仍从旧代际启动的情况;仅看Git或代际编号,不能判断正在使用的内核。

安排时间与构建资源
#

我把常规更新安排成手动维护窗口,按使用情况选择两周或一个月左右的间隔;这是维护约定,不是已经启用的自动升级任务。紧急修复则需要另外安排。

有些更新只重新生成配置,有些会构建内核或大型依赖。之前记录过一次内核构建耗时数小时,并因空间不足失败。因此要先确认可用空间、临时目录和可保留的恢复代际,再开始耗时构建。这个经验不能推导成所有电脑都需要相同时间或空间。

远程构建可以作为资源不足时的选择,但需要另外配置构建机身份、平台与信任关系,也不会替我们激活目标电脑。不能把构建、传输和部署当作同一件事。相关说明见Nix远程构建文档。

更新flake输入
#

在一个地方更新
#

选一个干净的main工作区作为协调位置:

1
2
3
4
5
git pull --ff-only
nix flake update
git diff -- flake.lock
git diff --check
nix flake check --no-build --show-trace

nix flake update更新锁文件,不会自动应用系统。分支仍由flake.nix决定;从一个发行分支切到另一个分支,需要另外检查升级说明。--no-build检查也不能证明每个软件包都能构建或硬件能正常使用。

如果只是明确更新一个输入,可以使用:

1
nix flake update nixpkgs

这是上一条全量更新的另一种选择,不需要机械地连续执行。依赖关系仍可能带来其他节点变化,所以应检查完整diff。命令用法见Nix输入更新说明。

记录部署版本
#

检查完成后,提交这次准备部署的输入:

1
2
3
4
git add flake.lock
git commit -m "Update reviewed flake inputs"
git push origin main
git rev-parse HEAD

如果还修改了模块,应将检查过的相应文件明确加入索引。最后输出的完整提交号用来标记这次部署的源码版本,不能用它代替目标机的激活证据。

目标电脑只同步这个已经检查过的版本,不再各自执行一次nix flake update。否则几台电脑可能取得不同时间的输入,后面排错时就难以比较。

逐台部署
#

同步目标电脑
#

在目标电脑的配置仓库中,可以这样检查:

1
2
3
4
rollout_revision="REPLACE_WITH_REVIEWED_COMMIT"
test -z "$(git status --porcelain)" && \
  git pull --ff-only && \
  test "$(git rev-parse HEAD)" = "$rollout_revision"

先替换占位符。这里要求工作区干净、同步可以快进,并且提交号与记录一致;有一步失败就先停下处理。远端如果已经出现更新的提交,也不能悄悄把它当成这次检查过的版本。可以整理出固定版本的工作区后再继续。

实际更新顺序会考虑能否现场恢复、桌面使用、计算任务和服务停机。我先选择容易观察和恢复的一台,验证后再继续,避免同时重启所有电脑。这个顺序是使用安排,不需要把某种机器型号固定成第一台。

构建与应用
#

在目标电脑上,用它自己的输出构建:

1
nixos-rebuild build --flake .#laptop --no-update-lock-file

--no-update-lock-file防止构建时顺便更新输入。远程SSH会话中,耗时操作可以放在已有的tmux会话里,以便断线后重新连接;它不会解决机器重启或磁盘不足的问题。

检查完成并准备应用时,再运行:

1
sudo nixos-rebuild switch --flake .#laptop --no-update-lock-file

switch会实际激活,并设置后续启动配置。构建或激活失败时,先停止继续部署其他电脑,保存错误信息并检查当前状态。激活可能已经改变了部分服务,不能假定失败就表示所有内容都没有变化。相关阶段见第五篇。

重启与验证
#

判断是否需要重启
#

切换之后,可以分别比较启动相关内容:

1
2
3
4
for component in kernel kernel-modules initrd; do
  readlink -f "/run/current-system/$component"
  readlink -f "/run/booted-system/$component"
done

如果内核或模块路径改变,应在保存工作、停止受影响任务后安排重启;initrd改变也需要通过新一次启动来验证。仅比较uname -r不够,相同版本号仍可能对应不同构建。

维护记录中也有当前代际与启动代际不同,但内核、模块和initrd路径相同的情况。这时不能仅因为代际不同就断言必须重启。其他驱动、服务或会话修改仍可能要求重启或重新登录,需要按修改内容判断。

验证目标功能
#

1
2
3
4
5
6
readlink -f /nix/var/nix/profiles/system
readlink -f /run/current-system
readlink -f /run/booted-system
uname -r
systemctl --failed
systemctl --user --failed

如果安排了重启,应重新连接并检查这次实际启动的系统。然后测试与这台电脑用途有关的功能:桌面登录、SSH、设备支持、计算任务或服务访问。只看到没有失败单元,不代表这些功能都已经测试过。

服务还要分开检查本机后端与实际访问入口;桌面设置也可能只在新会话中生效。把完成与待验证的项目分别记录,再决定是否继续下一台电脑。

应用仓库与子模块同步
#

实际维护中,应用源码、Compose定义和系统配置可能是不同的仓库。有些应用还是父仓库中的Git子模块,所以发布顺序也需要记录:

  1. 先提交并推送应用子仓库的修改;
  2. 再在父仓库记录已发布的子模块提交,推送父仓库;
  3. 目标电脑同步父仓库及其记录的子模块版本;
  4. 准备运行配置、部署对应服务,验证入口和功能。

父仓库只是记录子模块的提交号,不会自动把尚未发布的子仓库提交上传。同步源码也不会自动重建正在运行的应用镜像。

在工作区和子模块修改都已保存、确认可以更新时,可以使用:

1
2
3
git submodule update --init --recursive
git submodule status --recursive
git status --short

默认的子模块更新采用父仓库记录的提交,可能进入分离HEAD状态,不等于跟随子仓库最新分支。维护自己的子仓库分支时,应另外安排分支同步,不要用强制更新覆盖工作。相关说明见Git子模块文档。

有时远端访问或凭据工具不稳定,我也会用Git bundle传输已经发布的提交。它解决的是提交传输,不会自动证明目标工作区干净、子模块正确或服务已经部署;仍要核对版本并验证。

恢复与记录
#

新系统能够启动但不适合继续使用时,可以按恢复计划切回上一个系统profile代际:

1
sudo nixos-rebuild switch --rollback

这不是保证上一个代际一定可用。部署前应确认保留了已知可用的恢复目标;需要恢复旧内核时还要重新启动,无法启动时则依靠启动菜单中的可用代际。回滚不撤销Git提交,也不恢复应用数据。

更新记录中,我分别写下源码版本、激活结果、启动状态、功能检查,以及仍需处理的内容。只修改文档的提交不记成新系统代际;没有重启或没有测试的项目,也不写成已经验证。

确认正常使用一段时间后,再决定清理旧代际和无用构建引用。不要在更新失败时,先删除原本准备用于恢复的系统。

推荐的使用方法
#

我建议输入更新集中检查,目标电脑逐台部署,每台都使用已经确认的源码版本。普通配置、系统输入和应用部署分别处理,可以减少一次修改的范围,也容易知道问题出现在哪一步。

让AI协助时,可以要求它在开始前说明涉及哪些电脑、需要多久,以及是否会激活或重启。完成后按源码同步、构建、应用、启动和功能验证分别报告。这样下一次维护时,能知道哪些已经完成,哪些只是准备好了配置。

下一篇将回顾用AI维护NixOS的经验,包括哪些工作适合交给AI,以及怎样通过实际结果发现它的错误。

NixOS系列 - 这篇文章属于一个选集。
§ 12: 本文