在 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 自己推理该怎么做。"

发表评论 取消回复