在 DevOps 领域,"它在我机器上能跑" 的梗流传已久。传统的配置管理工具(Ansible、Chef、Puppet)虽然解决了一部分问题,但依赖地狱、隐式状态、漂移现象依然困扰着基础设施团队。NixOS 以一个激进的声明式方案切入,承诺真正的可重现性——这篇实战文章将从 Nix 表达式语言的内核机制出发,逐步深入 NixOS 模块系统、Flakes 锁定、生产部署以及与传统容器化方案的对比,帮助你在实际工程中落地这套工具链。


一、Nix 表达式语言:纯函数式包管理的心脏

Nix 包管理器的核心并非简单的二进制分发系统,而是一门惰性求值、纯函数式的领域特定语言(DSL)。理解这门语言是掌握 NixOS 基础设施的前提。

1.1 核心原语:Derivation 与 Store Path

在 Nix 的世界里,每一个构建产物都被建模为一个 Derivation—— 一个包含构建指令、依赖关系和输出路径的声明式描述。Nix Store(默认位于 /nix/store)使用内容寻址路径:

/nix/store/3x4f7b2a9c1d8e6a0b5f2d7c4e9a1b8c3d6f0a2e5-hello-2.12.1

路径中的哈希前缀由所有输入源(源码、编译器版本、依赖库、编译标志等)的递归哈希决定。这意味着:只要输入不变,构建产物必定相同——这就是 Nix 可重现性的数学基础。

用 Nix 表达式定义一个简单的 derivation:

let
  pkgs = import <nixpkgs> {};
in
pkgs.stdenv.mkDerivation {
  pname = "hello";
  version = "2.12.1";
  
  src = pkgs.fetchurl {
    url = "mirror://gnu/hello/hello-2.12.1.tar.gz";
    sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465";
  };
  
  buildInputs = [ pkgs.gcc ];
}

1.2 惰性求值与惰性数据结构

Nix 语言是惰性求值的——表达式只在需要时才计算。这个特性使得处理大规模包集合成为可能。例如 pkgs 集合包含了超过 80000 个包定义,但在一次 nix build 中只有直接依赖的表达式会被求值。

关键的惰性数据结构:

  • 列表:[ 1 2 3 ],支持惰性拼接
  • 属性集:{ a = 1; b = 2; },Nix 的 "字典" 结构,广泛用于函数参数传递
  • 字符串插值:"Hello ${name}",比 shell 拼接更安全的语法
  • with / let:作用域绑定的惯用法

理解 with pkgs; 这种模式——它把属性集的键引入当前作用域,是 Nix 代码中省略 pkgs. 前缀的常用写法。


二、NixOS 模块系统:声明式基础设施即代码

NixOS 不是一个"用 Nix 安装软件"的 Linux 发行版,而是一个完全由 Nix 表达式声明式定义的操作系统。整个系统的状态(服务配置、用户、内核参数、文件系统布局)都编码在一个统一的模块树中。

2.1 模块系统的组合数学

NixOS 模块系统的精妙之处在于它的组合语义。每个模块不是简单的配置文件,而是一个由 config、options、imports 三个核心字段构成的函数:

{ config, pkgs, lib, ... }:

{
  options.myService.enable = lib.mkOption {
    type = lib.types.bool;
    default = false;
    description = "是否启用我的自定义服务";
  };
  
  config = lib.mkIf config.myService.enable {
    systemd.services.myService = {
      description = "My Custom Service";
      serviceConfig = {
        ExecStart = "${pkgs.myService}/bin/my-service";
        Restart = "always";
      };
      wantedBy = [ "multi-user.target" ];
    };
  };
}

这种模块组合具有交换律和结合律——声明顺序不影响最终配置结果,这是声明式系统的数学保证。当你在 configuration.nix 中使用 imports = [ ./module-a.nix ./module-b.nix ]; 时,Nix 会在求值阶段自动合并所有模块的 options 声明和 config 片段。

2.2 实战:声明式 Kubernetes 工作节点

以下是一个真实的 NixOS 部署 Kubernetes 工作节点的配置片段:

{ config, pkgs, ... }:

{
  # 启用 containerd 运行时
  virtualisation.containerd = {
    enable = true;
    settings = {
      plugins.cri = {
        sandbox_image = "registry.k8s.io/pause:3.9";
        cni.bin_dir = "/opt/cni/bin";
      };
    };
  };
  
  # 配置 kubelet
  services.kubernetes = {
    roles = [ "node" ];
    proxy = {
      enable = true;
      hostname = config.networking.hostName;
    };
    kubelet = {
      hostname = config.networking.hostName;
      extraOpts = "--feature-gates=DevicePlugins=true";
      cni.packages = [ pkgs.cni-plugin-flannel ];
      taints = [
        { key = "dedicated"; value = "gpu"; effect = "NoSchedule"; }
      ];
    };
  };
  
  # 内核参数调优
  boot.kernel.sysctl = {
    "vm.max_map_count" = 262144;
    "net.bridge.bridge-nf-call-iptables" = 1;
    "net.ipv4.ip_forward" = 1;
  };
  
  # 分区与文件系统
  fileSystems."/" = {
    device = "/dev/disk/by-label/nixos";
    fsType = "ext4";
  };
}

关键洞察:这套配置并不是"安装脚本",而是系统最终状态的数学声明。NixOS 会根据你的声明自动计算需要创建哪些 systemd unit、需要安装哪些包、需要生成哪些配置文件。


三、Flakes:从"依赖地狱"到确定性构建

Flakes 是一个较新的 Nix 特性,解决了经典 nix-channel 的全局状态问题。它引入了 flake.nix(声明输入)和 flake.lock(锁定输入版本)的双文件确定性方案。

3.1 Flake 结构解析

一个典型的工程级 flake:

{
  description = "我的基础设施工具链";
  
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    home-manager.url = "github:nix-community/home-manager/release-24.05";
    flake-utils.url = "github:numtide/flake-utils";
    
    # 私有内部包
    internal-api.url = "git+ssh://[email protected]/myorg/internal-api";
    internal-api.inputs.nixpkgs.follows = "nixpkgs";
  };
  
  outputs = { self, nixpkgs, home-manager, flake-utils, internal-api, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            colmena       # NixOS 部署工具
            nixos-rebuild 
            sops-nix      # 机密管理
            terrahelp     # Terraform 辅助
          ];
        };
        
        packages.terraform-provider-mine = 
          pkgs.callPackage ./nix/terraform-provider.nix { };
      }
    ) // {
      # NixOS 系统配置输出
      nixosConfigurations.web-prod = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/web-prod/configuration.nix
          home-manager.nixosModules.home-manager
          sops-nix.nixosModules.sops
        ];
      };
      
      # 部署规范
      colmena = {
        meta = {
          nixpkgs = import nixpkgs { system = "x86_64-linux"; };
          nodeSpecialArgs = {
            "web-prod" = { inherit internal-api; };
          };
        };
        
        web-prod = { name, nodes, ... }: {
          deployment = {
            targetHost = "10.0.1.10";
            targetUser = "deploy";
            buildOnLocal = true;
          };
          imports = [ ./hosts/web-prod/configuration.nix ];
        };
      };
    };
}

3.2 锁定机制与二进制缓存

flake.lock 使用 JSON 格式记录每个输入的精确 Git commit SHA 和 Nix 内容哈希,确保:

  • 团队协作时所有成员的构建一致
  • CI/CD 环境与本地环境行为相同
  • 源码依赖更新显式可控(nix flake lock --update-input nixpkgs)

Nix 通过 nix.substituters 配置二进制缓存服务器。在生产环境中,自建二进缓存(如 Cachix 或 Harmonia)能将部署构建时间从 30 分钟缩短到 30 秒——因为所有预编译包直接从缓存拉取。


四、生产级 NixOS 部署实战:Colmena 与渐进式发布

当系统配置声明完成,下一步是部署到远程主机。colmena 是专为 NixOS 设计的声明式部署工具,比传统的 nixos-rebuild switch --target-host 更具工程化特性。

4.1 蓝绿部署策略

{
  colmena = {
    # 使用了 NixOS 的原子切换特性
    web-prod-blue = { ... }: {
      deployment = {
        targetHost = "10.0.1.11";
        allowLocalDeployment = false;
        tags = [ "web" "blue" ];
        
        # 健康检查
        healthChecks = {
          http = [
            {
              scheme = "https";
              port = 443;
              path = "/healthz";
              description = "API gateway health check";
            }
          ];
        };
      };
      imports = [ ./hosts/web-prod/configuration.nix ];
    };
    
    web-prod-green = { ... }: {
      deployment = {
        targetHost = "10.0.1.12";
        tags = [ "web" "green" ];
      };
      imports = [ ./hosts/web-prod/configuration.nix ];
    };
  };
}

部署流程变为:

# 构建但不激活
colmena build

# 逐步推送
colmena apply --on web-prod-green

# 验证流量切换后的健康状态
curl https://www.ybb.press/healthz

# 通过 DNS 或负载均衡器切换流量
# 如果出错,回滚只需一个命令
colmena rollback --on web-prod-blue

4.2 机密管理:sops-nix

生产部署中最敏感的问题之一是机密管理。sops-nix 将 Mozilla SOPS 密钥管理系统集成到 NixOS 模块中:

{ config, ... }:

{
  sops = {
    defaultSopsFile = ./secrets/production.yaml;
    age.keyFile = "/var/lib/sops-nix/key.txt";
    
    secrets = {
      "api_keys/staging_db" = {
        owner = "app";
        group = "app";
        mode = "0400";
        restartServices = [ "myapp.service" ];
      };
      "tls/wildcard_cert" = {
        owner = "nginx";
        path = "/etc/ssl/certs/fullchain.pem";
      };
    };
  };
}

关键设计:加密的 YAML 文件可以安全提交到 Git 仓库,只有在目标主机上(通过 age 私钥解密)才能还原成实际机密文件。这解决了"加密密钥本身如何传递"的鸡生蛋问题。


五、Nix vs Docker:为什么不是互斥的选择

很多工程师会问:"我已经用了 Docker/K8s,还需要 NixOS 吗?" 答案是两者互补而非替代。

5.1 镜像构建的确定性

传统 Dockerfile 的问题在于:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl jq  # 非确定!

apt-get update 每次拉取的包列表可能不同,latest 标签更是不确定的。Nix 则天然适合构建 OCI 镜像:

dockerImage = pkgs.dockerTools.buildImage {
  name = "my-app";
  tag = "latest";
  
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    paths = [ 
      pkgs.coreutils 
      pkgs.curl 
      pkgs.jq 
      myAppPackage 
    ];
    pathsToLink = [ "/bin" "/lib" ];
  };
  
  config = {
    Cmd = [ "/bin/my-app" ];
    Env = [ "PATH=/bin:/usr/bin" ];
  };
};

生成的镜像层哈希完全由包的内容决定,确保每个 commit 构建出 bit-for-bit 相同的镜像。

5.2 嵌入式部署场景

在 IoT 和嵌入式 Linux 领域,Nix 的优势尤为突出——你可以用同一个工具链从交叉编译工具链(pkgsCross.aarch64-multiplatform)一直到系统镜像打包(pkgs.spliceableLinux),保持完整确定性:

{
  # 为 Raspberry Pi 构建完整系统镜像
  raspberryPiImage = (import <nixpkgs/nixos> {
    configuration = { config, pkgs, ... }: {
      imports = [ <nixpkgs/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix> ];
      
      boot.kernelPackages = pkgs.linuxPackages_rpi4;
      
      # 交叉编译所需设置
      nixpkgs.crossSystem = {
        system = "aarch64-linux";
        config = "aarch64-unknown-linux-gnu";
      };
    };
  }).config.system.build.sdImage;
}

六、实战踩坑与工程最佳实践

6.1 Nixpkgs 算子优先级陷阱

Nix 语言中 // 和 mkOverride 的优先级容易混淆:

# mkOverride 优先级:50 < 100 < 1000
# 数值越低优先级越高
{
  services.nginx.enable = lib.mkOverride 50 true;  # 被用户覆盖
  services.nginx.enable = lib.mkDefault false;      # 默认不启用(优先级 1000)
}

理解 mkForce(优先级 5)、mkOverride(用户定义优先级)、mkDefault(1000)的级联顺序,能避免许多"我的配置明明写了为什么没生效"的困惑。

6.2 磁盘空间治理

Nix Store 的内容寻址特性意味着一旦某个 derivation 被需要,就会一直保留直到 GC。在生产环境中:

# /etc/nixos/configuration.nix
{
  # 启用自动 GC
  nix = {
    gc = {
      automatic = true;
      dates = "weekly";
      options = "--delete-older-than 30d";
    };
    
    # 限制 /nix/store 占用
    settings.max-free = 10 * 1024 * 1024 * 1024;  # 10GB
    settings.min-free =  1 * 1024 * 1024 * 1024;  # 1GB
    
    # 二进制缓存
    substituters = [
      "https://cache.nixos.org"
      "https://my-org.cachix.org"
    ];
    trustedPublicKeys = [
      "my-org.cachix.org-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="
    ];
  };
}

6.3 Post-Deploy Hook 的执行时机

NixOS 的 switch-to-configuration 脚本会在 build 之后执行:

• pre-switch:在切换前(发送通知)

• switch:原子切换新 generation,必要时重启服务

• post-activate:所有服务启动后(适合健康验证、监控 registration)

合理放置 post-deploy hook 能实现零停机发布。


七、总结:NixOS 的工程哲学

NixOS 不仅仅是一套工具链,更是一种基础设施哲学的可执行实现:

  • 纯函数式配置 = 声明式 + 可推导 + 可测试
  • 内容寻址存储 = 原子升级 + 瞬时回滚 + 去重
  • 模块系统 = 组合而非继承、类型安全而非字符串拼接
  • Flakes = 时间冻结的确定性

对于追求"基础设施即终极代码"的团队,NixOS 提供了一条从开发机到生产服务器的完整可重现路径。它学习曲线陡峭,但一旦掌握,你会发现"环境不一致"这个问题被从根上消除了。

正如 Nix 社区一句广为流传的话:"过去你用脚本改变机器;现在你描述期望状态,让 NixOS 自己推理该怎么做。"

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部