Flakes 最佳实践:企业级布局与模块化拆分

Flakes 已成为 Nix 生态的事实标准,但「能用」和「用得对」是两回事。本文从企业工程视角总结 inputs/outputs 组织、flake.lock 锁文件治理、多文件模块化拆分、模板复用与 monorepo 级 flake 布局,附带可直接落地的目录骨架与代码。

1. 从「能用」到「可维护」

https://plumephp.com/nix-flakes/ 讲解了 flake.nix 的基本结构与命令;本文解决的是更高一层的问题:当 flake 从一个人练习变成多人、多项目、跨团队的共享基础设施时,怎么组织才不会烂掉?

我们在多个生产仓库中总结出的共性痛点:

  • flake.nix 膨胀到上千行,没人敢动
  • inputs 的 follows 关系混乱,flake.lock 频繁冲突
  • 包、devShell、NixOS 模块、CI 检查全部堆在一个文件里
  • 团队间复用配置靠复制粘贴,版本永远对不上
  • nix flake update 一次升级全部依赖,回归爆炸

本文给出的一套可落地的最佳实践,配合 https://plumephp.com/nix-language-deep-dive/ 的语言功底与 https://plumephp.com/nix-ci-cachix/ 的 CI 流水线,构成完整的工程化闭环。

📌 相关专题:多仓库依赖治理可参考 https://plumephp.com/posts/devops/;CI 集成见 https://plumephp.com/posts/github-actions/。


2. inputs 组织:依赖治理的第一道闸门

2.1 依赖分类与命名约定

inputs 建议按功能前缀分组命名,一目了然:

inputs = {
  # 基础:nixpkgs 是唯一真正的「运行时」
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";

  # 工具链:devShell 与 CI
  devenv.url = "github:cachix/devenv";
  flake-utils.url = "github:numtide/flake-utils";

  # 平台配置:NixOS / macOS
  home-manager.url = "github:nix-community/home-manager";
  nix-darwin.url = "github:LnL7/nix-darwin";

  # 基础设施:本组织的私有 flake(Git 私有仓库)
  my-common = {
    url = "git+ssh://git@github.com/my-org/my-common?ref=main";
    inputs.nixpkgs.follows = "nixpkgs";
  };
};

2.2 follows 治理:一个 nixpkgs 原则

「一个项目只应有一个 nixpkgs 主版本」是避免锁文件膨胀的第一原则。所有相互配合的 flake 都应该 follows 到同一个 nixpkgs:

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";

  # 常见错误:不写 follows,home-manager 会拉自己的 nixpkgs
  # 正确做法:显式共享
  home-manager = {
    url = "github:nix-community/home-manager";
    inputs.nixpkgs.follows = "nixpkgs";
  };
  nix-darwin = {
    url = "github:LnL7/nix-darwin";
    inputs.nixpkgs.follows = "nixpkgs";
  };
  # 工具链也 follow
  devenv = {
    url = "github:cachix/devenv";
    inputs.nixpkgs.follows = "nixpkgs";
  };
};

用 nix flake metadata 检查是否有「漏网」的独立 nixpkgs:

# 列出依赖树,检查是否出现多个不同 rev 的 nixpkgs
nix flake metadata --json | jq '.locks.nodes | to_entries[] | select(.key|contains("nixpkgs")) | .value.locked.rev'

2.3 需要两个 nixpkgs 时:显式命名

某些场景(如生产系统用稳定版、devShell 用 unstable 的新工具)需要两个 nixpkgs,此时必须显式命名并在使用点明确选择:

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";      # 生产
  nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";  # 工具
};

outputs = { self, nixpkgs, nixpkgs-unstable }:
  let
    system = "x86_64-linux";
    pkgs = nixpkgs.legacyPackages.${system};
    pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
  in {
    devShells.${system}.default = pkgs.mkShell {
      # 混用:多数用稳定,个别用 unstable
      packages = with pkgs; [ nodejs_20 yarn ] ++ [ pkgs-unstable.ripgrep ];
    };
    packages.${system}.default = pkgs.hello;   # 构建产物用稳定
  };

3. outputs 组织:少而明确的出口

3.1 最小化「顶层杂货铺」

outputs 是 flake 对外的公共 API,建议只暴露这五类,其余内部逻辑全部拆到子文件(见第 4 节):

输出用途团队约定
packages.<sys>.default构建产物(唯一默认包)一个 flake 只产出一个默认包
devShells.<sys>.default开发环境与 devenv/direnv 配合
nixosConfigurations.*NixOS 机器机器按主机名命名
checks.<sys>.*CI 检查项与 GitHub Actions 一一对应
overlays.default对外暴露的 nixpkgs 覆盖复用方通过 overlay 接入
outputs = { self, nixpkgs, flake-utils }:
  flake-utils.lib.eachDefaultSystem (system:
    let pkgs = nixpkgs.legacyPackages.${system};
    in {
      packages.default = pkgs.callPackage ./pkgs/default.nix {};
      devShells.default = import ./shells/default.nix { inherit pkgs; };
      checks.default = import ./checks/default.nix { inherit pkgs; };
    }
  ) // {
    # 跨系统的输出放在 eachDefaultSystem 之外
    overlays.default = import ./overlays/default.nix;
    nixosModules.default = import ./modules/nixos.nix;
  };

3.2 eachDefaultSystem 的正确姿势

flake-utils 的 eachDefaultSystem 是省模板的利器,但它只遍历四个默认系统;不要在它里面放与系统无关的东西:

{
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system: {
      # ✅ 系统相关:packages / devShells / checks / apps / formatter
      packages.default = ...;
      devShells.default = ...;
    })
    // {
      # ✅ 系统无关:overlays / nixosModules / nixosConfigurations / templates
      overlays.default = ...;
      nixosModules.default = ...;
      templates.default = ...;
    };
}

4. 模块化拆分:把 1000 行拆成 100 行

4.1 推荐目录骨架

一个中型仓库的建议结构(避免子目录过深,保持扁平):

repo/
├── flake.nix               # 总入口:只做接线(wiring)
├── flake.lock              # 锁定文件,提交进 Git
├── inputs.nix              # 1. inputs 集中定义
├── outputs/
│   ├── packages.nix        # 2. 包定义出口
│   ├── devshells.nix       # 3. devShell 出口
│   ├── checks.nix          # 4. CI 检查出口
│   ├── overlays.nix        # 5. overlay 出口
│   └── nixos.nix           # 6. NixOS 配置出口
├── pkgs/
│   ├── default.nix         # callPackage 统一入口
│   ├── myapp.nix           # 单个包的 derivation
│   └── mylib.nix
├── shells/
│   ├── default.nix
│   └── ci.nix              # CI 专用精简 shell
├── checks/
│   ├── nixfmt.nix
│   └── test.nix
├── modules/
│   └── nixos.nix
└── templates/
    └── default/            # 团队模板
        ├── flake.nix
        └── ...

4.2 flake.nix 只做「接线」

# flake.nix —— 真正的总入口只有二十几行
{
  description = "Acme 核心服务 monorepo";

  inputs = import ./inputs.nix;

  outputs = inputs@{ self, nixpkgs, flake-utils, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    flake-utils.lib.eachDefaultSystem
      (system:
        let pkgs = nixpkgs.legacyPackages.${system}; in {
          packages = import ./outputs/packages.nix { inherit pkgs; self; };
          devShells = import ./outputs/devshells.nix { inherit pkgs; };
          checks = import ./outputs/checks.nix { inherit pkgs; };
        })
      // {
        overlays = import ./outputs/overlays.nix;
        nixosModules = import ./outputs/nixos.nix;
        nixosConfigurations = import ./outputs/nixos.nix { inherit nixpkgs; };
      };
}

4.3 子文件保持「纯函数」风格

每个子文件只接收显式参数、返回明确的 attrset,不隐藏全局依赖:

# outputs/packages.nix
{ pkgs, self }:
{
  default = pkgs.callPackage ../pkgs/default.nix { };
  mylib = pkgs.callPackage ../pkgs/mylib.nix { };
}
# outputs/devshells.nix
{ pkgs }:
{
  default = import ../shells/default.nix { inherit pkgs; };
  ci = import ../shells/ci.nix { inherit pkgs; };
}

最佳实践:子文件不 import <nixpkgs>、不用 with、不读环境变量。所有输入都从 flake.nix 传入,让整个配置「纯」到可以测试——这也是后续做单元检查的基础。


5. flake.lock:锁定文件治理

5.1 锁文件是「可复现」的保险单

flake.lock 必须提交进 Git。CI 里应该用 nix flake build .#... 直接消费它,保证构建与本地一致:

# 查看当前锁定的每个 input
nix flake lock --show-lock-file

# 只更新某个 input(避免一次全量升级)
nix flake update nixpkgs

# 锁定到当前解析(不升级,只固化)
nix flake lock

# 检查 lock 是否与 flake.nix 一致
nix flake check

5.2 升级策略:分层滚动

场景更新方式回归控制
日常开发nix flake update <单个 input>小步、可回退
稳定版 nixpkgs跟踪 nixos-24.05 这类稳定分支lock 自动锁住,天然安全
安全补丁手工 nix flake update nixpkgs构建所有 checks
团队大版本改 URL → nix flake lock → 全量 checksPR 级评审

5.3 合并冲突的止血方案

多人在同一分支改 flake.nix 时,lock 冲突常见。策略:

  1. 代码评审时不让人手改 lock:flake.nix 的修改与 lock 的刷新绑定(nix flake lock 在同一 PR 完成)
  2. 冲突时以 flake.nix 为准:git checkout --theirs flake.lock && nix flake lock 重新解析
  3. 利用 nix flake update --recreate-lock-file:彻底重建,适合 lock 已严重损坏的场景
# 重建 lock(谨慎:会丢失精确锁定,尽量只在冲突无法解决时用)
nix flake update --recreate-lock-file

6. 模板复用:让团队从同一张白纸开始

6.1 内建 templates 输出

flake 可以内置 templates,让 nix flake init -t 直接创建标准骨架:

# flake.nix 追加
templates = {
  default = {
    path = ./templates/default;
    description = "Acme 标准服务骨架";
  };
  library = {
    path = ./templates/library;
    description = "Acme 纯库骨架";
  };
};
# 使用
nix flake init -t github:my-org/my-common#default
nix flake init -t github:my-org/my-common#library

6.2 模板内容要点

templates/default/flake.nix:

{
  description = "Acme 标准服务骨架";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in {
        packages.default = pkgs.callPackage ./pkgs/default.nix { };
        devShells.default = import ./shells/default.nix { inherit pkgs; };
      });
}

注意:模板目录里的 flake.nix 也会被 nix 解析。若 templates 指向的目录包含顶层 flake.nix,nix 会要求它自身可求值——为避免「模板模板」复杂度,模板目录内不要再嵌套独立 flake,只用扁平 .nix 文件。

6.3 模板更新:单一来源

模板一旦被多个项目使用,更新模板后旧项目不会自动同步。两个治理手段:

  1. 把模板的「核心」抽成公共 flake(如 my-common),模板只做薄壳
  2. 定期跑 nix flake init -t ... 到临时目录比对差异,用 diff 手工合入

7. 企业级布局:多服务的 monorepo

7.1 场景与取舍

一个仓库承载:多个可独立部署的服务、共享的 NixOS 模块、统一的 devShell 与 CI。

方案优点缺点适用
单一 flake依赖锁定统一,开发体验一致构建量大、升级联动中小 monorepo(本文方案)
嵌套子 flake服务间依赖隔离lock 各自独立,跨服务升级复杂服务边界极强的超大仓库
独立仓库 + 公共 flake权限/发布独立依赖发布流程跨团队、跨组织

7.2 单一 flake monorepo 骨架

acme-monorepo/
├── flake.nix
├── inputs.nix
├── outputs/
│   ├── packages.nix
│   ├── devshells.nix
│   ├── checks.nix
│   └── nixos.nix
├── pkgs/
│   ├── default.nix
│   ├── service-a.nix
│   └── service-b.nix
├── services/
│   └── nixos-modules/
│       ├── service-a.nix
│       └── service-b.nix
├── hosts/
│   ├── prod-a.nix
│   └── prod-b.nix
├── shells/
│   ├── default.nix
│   └── ci.nix
└── checks/
    ├── nixfmt.nix
    ├── build.nix
    └── test.nix

7.3 跨服务共享的「内部库」

共享配置通过 outputs/nixos.nix 暴露 nixosModules,服务主机 import 它:

# outputs/nixos.nix
{ pkgs, lib }:
{
  nixosModules.service-base = import ../services/nixos-modules/base.nix;
  nixosModules.service-a = import ../services/nixos-modules/service-a.nix;

  nixosConfigurations = {
    prod-a = lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ../hosts/prod-a.nix
        (import ../services/nixos-modules/service-a.nix)
      ];
    };
    prod-b = lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ../hosts/prod-b.nix
        (import ../services/nixos-modules/service-b.nix)
      ];
    };
  };
}

8. 常见反模式与修正

反模式问题修正
顶层 outputs 写 800 行难以 review、复用拆 inputs.nix + outputs/*.nix
每个 input 都 follows 或都不 followslock 膨胀 / 版本不一致遵循「一个 nixpkgs」原则,例外显式命名
手改 flake.lock版本漂移、冲突一律 nix flake lock/update 生成
子文件 import <nixpkgs>通道依赖、不可复现全部从 flake 传入
模板目录嵌套 flake解析混乱模板内只放扁平 .nix
nix flake update 无差别全升回归爆炸单 input 升级 + checks 把关

9. 总结

Flakes 工程化的核心不是某个语法技巧,而是把「接线」与「实现」分离的纪律:

  • inputs:显式命名、单 nixpkgs 优先、follows 显式声明
  • outputs:少而明确的公共 API,系统无关输出放 eachDefaultSystem 之外
  • 结构:flake.nix 只接线,inputs.nix 与 outputs/*.nix 承载实现
  • 锁定:lock 提交 Git、CI 消费 lock、单 input 滚动升级
  • 复用:templates 统一起点,overlays/nixosModules 作为公共资产输出

掌握这套布局后,与 https://plumephp.com/nix-ci-cachix/ 的缓存流水线、https://plumephp.com/nixos-module-system/ 的模块化开发配合,就能把 Nix 从「个人玩具」升级为「组织级基础设施」。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力