LokiSharp

我让 AI 参与维护 9 台 NixOS(上):为什么声明式系统适合人机协作

  •  
  •   LokiSharp · 16h 42m ago · 388 views

    本文不是一篇“让 AI 帮我写了几个配置文件”的体验文,而是一份持续协作记录:我让 AI 参与维护一套真实运行的 NixOS 基础设施,经历需求讨论、代码审查、静态检查、测试、 金丝雀部署、全节点发布和线上告警处理。它确实提高了效率,也确实犯过一些只有人类知道 为什么不能犯的错误。

    本文涉及的公开配置与实现:LokiSharp/nix-config。 真实凭据保存在独立私有仓库中,不包含在公开配置里。

    写在前面

    先说明两点。

    第一,本文中的“AI”不是一个被接入生产环境后完全自主行动的机器人。它运行在我的工作区中, 能读取仓库、修改文件和执行检查;只有在我明确授权后,才会提交、连接节点或部署。敏感值由 SOPS 管理,我不会因为调试方便就把明文 secret 交给它。

    第二,这篇文章本身也由 AI 参与整理。事实材料来自真实仓库、提交历史、部署输出、监控邮件 和我们之间的连续对话;章节结构和初稿由 AI 生成,我负责提供语境、纠正错误和决定哪些经验 值得公开。换句话说,文章的产生方式就是文章主题的一部分。

    如果不想读完整连载,可以先记住下面五点:

    1. NixOS 最适合 AI 的地方,不是 Nix 语法容易生成,而是系统状态能被求值、构建、测试、 比较和回滚;
    2. AI 最有价值的工作不是一次写出完整配置,而是持续做仓库搜索、约束翻译、机械重构、 故障调查和文档同步;
    3. nix flake check 通过只代表验证链的一层,真实部署仍然需要 Test 金丝雀、目标节点探针 和一段时间的监控;
    4. AI 会删除有意保留的模板、添加恒真测试、误解历史地址、把告警安静误当成故障解决, 所以权限边界和停止条件必须写清楚;
    5. 最可靠的合作方式是让人负责目标与授权,让 AI 负责搜索和执行,让 Nix 、Git 、测试、 Test 节点与监控分别提供不同层次的证据。

    本文不会证明“NixOS 是唯一正确的发行版”,也不会证明“AI 已经能替代运维工程师”。我想 讨论的是一个更具体的问题:当系统本身可以被声明、检查和回滚时,我们是否能用一种比 “复制 AI 给出的命令并祈祷”更成熟的方式与它合作?

    一、先说结论

    过去一段时间,我一直在尝试让 AI 深度参与自己的 NixOS 配置仓库。

    这里的“深度参与”不是让它生成一段 configuration.nix,然后由我复制粘贴;而是让它进入 仓库,阅读现有模块,理解主机模型,修改代码,运行格式化和静态检查,补测试,按照提交 规范拆分 commit ,先部署到 Test 节点,验证无误后再推向其他节点,最后继续读取 systemd 、 Prometheus 和 Alertmanager 的反馈。

    目前这套仓库管理:

    • 9 台 NixOS 节点;
    • 1 台 nix-darwin 设备;
    • 裸机服务器、虚拟机、测试节点以及多个不同供应商的 VPS ;
    • 261 个 Nix 文件;
    • 30 组 x86_64-linux 求值测试;
    • Home Manager 、Colmena 、Disko 、impermanence 、sops-nix ;
    • BIRD 、DN42 、ZeroTier 、Tailscale 、sing-box 、Caddy 、PostgreSQL 、Gitea 、Grafana 、 VictoriaMetrics 、Alertmanager 等服务;
    • Btrfs 快照、scrub 、SMART 、mdraid 、coredump 和部署后健康检查。

    我的结论是:

    NixOS 并不会让 AI 自动变得可靠,但它会把 AI 的大量错误提前变成可观察、可比较、可拒绝 的结果。

    这两者的组合真正有价值的地方,不是“AI 会写 Nix”,而是 NixOS 把系统状态变成了代码, 又把代码变成了一套可以求值、构建、比较、回滚和部署的闭环。

    如果使用普通发行版,AI 可能会告诉你执行十几条命令、修改五个路径下的配置、重启三个 服务。命令执行完以后,机器究竟处于什么状态,需要靠人记忆。

    在 NixOS 中,更理想的合作方式是:

    1. 人描述目标、约束和不可接受的风险;
    2. AI 阅读仓库中的现有事实;
    3. AI 修改声明式配置;
    4. Nix 对配置进行求值;
    5. 测试检查跨节点约束;
    6. Test 节点承担真实运行验证;
    7. 监控系统提供部署后的长期反馈;
    8. 失败时回到上一个 generation 或上一个 commit 。

    它仍然不是“按一下按钮全自动运维”,但已经很接近一种可审计的人机协作工程。

    二、为什么偏偏是 NixOS

    1. AI 最擅长操作文本,而 NixOS 恰好把系统变成文本

    AI 对当前机器里“曾经运行过哪些命令”没有天然记忆,也很难仅凭 /etc、数据库和服务 状态还原管理员过去几年的意图。

    但它很擅长:

    • 搜索代码;
    • 比较相似模块;
    • 找出重复结构;
    • 根据类型和约束补全字段;
    • 把手工流程整理成函数;
    • 根据错误信息做局部修正;
    • 为已经明确的规则生成测试。

    NixOS 把软件包、用户、systemd unit 、内核参数、防火墙、文件系统、服务配置和部署元数据 都放进同一个表达式系统。这意味着 AI 不必先猜“这台机器可能被手动改过什么”,而可以从 仓库中得到一个相对完整的意图模型。

    Nix 官方文档把 Nix 的典型使用场景概括为可复现开发环境和 Linux 机器的声明式定义; Flake 又提供了统一入口、输入锁定和标准化输出。对 AI 来说,这些恰好意味着更稳定的上下文: 依赖版本、主机输出和测试入口都在仓库里,而不是散落在聊天记录中。

    2. Nix 的失败通常发生得比较早

    传统运维脚本的典型风险是:前八步成功,第九步失败,机器停在一种很难描述的中间状态。

    Nix 当然也可能在 activation 阶段失败,但大量问题会更早暴露:

    • 语法错误在解析阶段暴露;
    • option 类型错误在模块求值阶段暴露;
    • 不存在的属性在 evaluation 阶段暴露;
    • package 、unit 、secret 声明可以在构建阶段暴露;
    • 跨节点重复地址可以通过纯求值测试暴露;
    • Prometheus 规则可以通过 Promtool 暴露;
    • Nushell 的错误数据形状可以通过类型签名和测试暴露。

    这对 AI 特别重要。AI 最大的问题通常不是完全不会,而是“看起来很像对的”。越早让机器 检查它,越不需要依赖人类逐字阅读几千行差异。

    3. 旧 generation 给试错留下了空间

    NixOS 的代际模型让一次系统切换不会直接覆盖所有旧状态。只要启动链、磁盘和远程入口还 在,很多错误都可以切回上一代。

    这并不意味着可以让 AI 随便部署。错误的防火墙、磁盘布局、SSH 配置和 secret 仍然可能 把人锁在门外。但是相较于不可追踪的命令历史,generation 至少让“刚才那次系统变更”有 一个清楚边界。

    4. Flake 既是入口,也是边界

    我的仓库使用 Flake 管理输入和输出。AI 可以从 flake.nix 开始理解:

    • 使用哪些 nixpkgs 分支;
    • 哪些输入是公开依赖;
    • 哪个输入来自私有 secrets 仓库;
    • 有哪些 NixOS 、Darwin 、package 、check 和 devShell 输出;
    • CI 和本地执行的求值是否一致。

    这里也有一个很实际的坑:Git 仓库中的 Flake 默认只看到已跟踪或已暂存的文件。

    我曾经让 AI 新增一组测试。它运行 just test,所有测试都通过了,但报告里没有新测试。 原因不是测试写得好,而是新文件还没有进入 Git ,Flake 根本没看见它。

    后来流程被修正成:

    1. 新增测试文件;
    2. 确认 Git 能看到它;
    3. 重新运行求值;
    4. 必须在测试报告中明确看到新测试名称;
    5. 才能说“新增测试通过”。

    这是一个非常典型的例子:AI 会把“命令退出码为 0”理解为成功,而工程系统必须继续追问 “我们想测的东西真的参与测试了吗?”

    三、我的仓库不是从一开始就这么整齐

    这套配置最早也有大量常见问题:

    • 主机元数据散落在不同文件;
    • index 、地址和部署标签之间没有统一约束;
    • 一些测试只是把实现重新抄一遍;
    • 部署依靠单节点命令,没有强制 Test 金丝雀;
    • 健康检查默认使用高权限账户;
    • CI 只能求值,无法发现部分真实构建问题;
    • 静态检查加入后,预留模板被误判为无用代码;
    • 网络、日志和监控告警有不少历史噪声;
    • 文档只覆盖局部目录,新接手者很难得到全局图。

    AI 真正带来的改变不是一次“大重构”,而是把这些模糊的不舒服逐步变成具体问题,然后一项 一项处理。

    这个过程持续了很多轮对话。很多时候,我只会问:

    接下来还有什么可以优化?

    AI 会先读代码,列出若干候选;我再问每一项的意义、代价和风险。确认以后,只改其中一项, 测试、提交、部署,再讨论下一项。

    这种节奏比让 AI 一次生成一个“完美架构”可靠得多。

    四、案例一:从“这个节点该用几号”到全局地址约束

    我的多节点网络里,主机 index 不只是一个展示字段,它还参与生成多个内部网络地址、部署 标签和 DNS 记录。

    一次讨论从一个很小的问题开始:某个节点应该使用哪个编号。

    AI 先分析现有地址分配逻辑,又解释 /26 的地址范围、主机位和可用地址。我们讨论过是否 把整个序号换掉,最后给 OVH 节点选择了 7 。

    如果只是修改一个数字,这件事没有太大意义。真正有价值的是随后补出的约束:

    • 每个主机 index 全局唯一;
    • 由 index 派生的各网络地址不能重复;
    • 启用某网络时,相关字段必须完整;
    • ZeroTier node ID 不能在两个主机上复用;
    • DNS 记录必须覆盖所有应该出现的节点;
    • 已退休节点不能继续残留在输出中。

    最后,主机元数据从“方便写配置的 attrset”变成了一种小型数据模型。

    AI 在这里的优势很明显:它很适合沿着字段依赖关系搜索所有消费者,然后生成跨节点测试。

    但它也暴露了一个危险倾向:喜欢加“看起来更保险”的判断。

    例如它曾经加入:

    benchmarkIPv4RoutesAbsent =
      lib.all (route: !lib.hasPrefix "198.19." route.target) routes;
    

    问题是,我的 ZeroTier 正常地址使用的是 198.18.<index>.0/24,而 198.19.* 已经不再使用。 这条检查虽然会通过,却没有保护任何当前需求。它只是检查一个已经不存在的历史路径。

    我追问:

    为什么要加这个判断?这个是我 ZeroTier 的地址。

    继续讨论以后,我们删掉了这类冗余测试。

    这件事让我形成了一个很重要的判断标准:

    测试不能只证明“某个字符串不存在”,它必须对应一个仍然存在的风险。

    AI 很容易生成大量断言,但断言数量不等于保障强度。

    五、案例二:SSH 不能只检查“服务开着”

    我最不能接受的部署事故之一,是远程修改后把自己锁在服务器外面。

    仓库原来有一个类似 public-ssh-port.nftablesEnabled 的检查。我没有同意删除它,反而要求:

    这个还是检查吧,我不想被关在外面。是不是要再扩展一下,从各个角度保证 SSH 端口开放?

    于是 SSH 保障不再只是检查一个布尔值,而是从多个层面验证:

    • OpenSSH 服务启用;
    • 实际监听端口与主机元数据一致;
    • 防火墙允许同一个端口;
    • 公开暴露面期望值与配置一致;
    • 部署健康元数据携带正确端口;
    • Colmena 的目标端口没有走另一条遗留配置;
    • 所有公网节点遵循同一约束。

    这正是 NixOS 与 AI 配合得很舒服的地方。

    如果用普通脚本,AI 可能给出:

    systemctl status sshd
    ss -lntp
    iptables -L
    

    这些命令只能描述当前机器的一瞬间。

    在 NixOS 中,我们可以把“SSH 服务、端口、防火墙、部署目标和测试期望必须一致”写成仓库 长期成立的不变量。以后改端口时,只要遗漏任何一个消费者,求值测试就会失败。

    当然,这仍然不能完全替代外部探测。内部网络访问成功,不等于公网防火墙路径正确。因此 仓库还保留了从不受信网络执行 public exposure 检查的入口。

    声明式测试和真实网络探测并不是二选一:前者检查意图一致性,后者检查现实世界。

    六、案例三:让 Test 节点成为真正的发布闸门

    最初的部署方式是 just <HostName>:选一个节点,交给 Colmena 。

    这种方式对人来说很直接,对 AI 来说却太自由。只要命令构造错一个目标,就可能直接修改 生产节点。

    后来我们把发布流程改成固定顺序:

    1. 完整求值 Flake ;
    2. 执行部署流程自身的可执行测试;
    3. 只部署 Test-NixOS
    4. 检查 Test 的 SSH 、主机名、systemd 、内核、audit 、ZeroTier 、BIRD 、DNS 和日志;
    5. Test 全部通过后,才部署剩余节点;
    6. 最后对所有节点执行健康检查。

    现在完整发布入口是:

    just deploy-all
    

    这里最关键的并不是命令变短,而是部署顺序从“聊天中的约定”变成了代码。

    为了验证这个顺序,我们还给部署编排本身写了测试:

    • Test 部署失败时,其他节点不能开始;
    • Test 健康检查失败时,其他节点不能开始;
    • 中途某节点部署失败时,不能伪装成全量成功;
    • 最终健康检查失败时,命令必须返回失败;
    • SSH multiplexing 失效时允许重建连接;
    • 认证失败和真实网络错误不能被错误地当作 multiplexing 问题重试。

    有一次真实发布中,所有节点都成功 activation ,但最终健康检查发现 MoeDove-TPE -> Test-NixOS 的 SLK IPv6 在十个包里丢了两个,超过 10% 阈值。

    发布命令因此返回失败。

    AI 没有把它说成“部署失败”,也没有立刻重新部署全部节点,而是区分:

    • 配置是否已经成功激活;
    • 服务是否正常;
    • 是哪一条健康检查失败;
    • 同一节点的 IPv4 和另一条 IPv6 overlay 是否正常;
    • 单独复测后丢包是否回落。

    复测时,SLK IPv6 丢包降到 10%,按策略记录 warning ; Loki-Net IPv6 无丢包,节点健康 检查通过。

    这类细节非常重要。一个好的自动化系统不应该只有“成功/失败”两个词,而应该保留失败发生 在哪一层。

    上篇小结

    这一篇先回答了“为什么是 NixOS”:AI 擅长处理文本和结构,NixOS 则把系统意图变成可以 求值、构建、测试和回滚的文本。主机编号、SSH 端口和部署顺序不再只是管理员脑中的约定, 而可以成为仓库中的长期约束。

    但让 AI 能修改配置,并不等于应该给它无限权限。下一篇会继续写健康检查账户、secrets 预留模板、Nushell 类型、Btrfs 快照、磁盘监控、Alertmanager 告警和测试反例,并进一步 讨论怎样给 AI 下达运维任务、如何判断它提供的证据是否足够。

    系列下一篇:《我让 AI 参与维护 9 台 NixOS (中):权限、故障案例与实战方法》

    本篇参考

    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   917 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 22:17 · PVG 06:17 · LAX 15:17 · JFK 18:17
    ♥ Do have faith in what you're doing.