Cloudflare开源cloudflare-os:基于NixOS的服务器级Linux发行版实践

1. 这个仓库到底是什么:Cloudflare 给自己造的系统长什么样

我第一次点开cloudflare-os这个 GitHub 仓库时,第一反应是:Cloudflare 不是做 CDN、DNS、WAF 的么,怎么突然搞起操作系统了?等我把 README 从头到尾看完,发现这事并没有想象中那么“突然”。

这个仓库是 Cloudflare 内部系统团队做的一个实验性 Linux 发行版,底座是 NixOS。它想解决的是一类很典型的问题:当你有成千上万台物理机、虚拟机、边缘节点,分布在全球各地,它们必须长期保持同一种行为,但你又不可能给每台机器都配一个运维人员随时登录上去敲命令。你要的是一套“可以像代码一样管理”的操作系统——配置即代码、构建可复现、升级可回滚、密钥可轮换、启动链路可验证。

如果你之前接触过 NixOS,基本能猜到它的核心思路:不把系统理解成“一堆文件复制到磁盘上”,而是理解成“一个从源代码推导出来的不可变整体”。cloudflare-os走的正是这个方向。它的价值不在于给普通用户一个桌面 Linux 发行版,而在于给需要大规模管理服务器的团队展示了一条完整的技术路径:用 NixOS 生态组织出可复现、可签名、可自动部署的操作系统镜像。

这篇文章适合三类人看:一是对 NixOS 有兴趣但还没想清楚怎么落地的 Linux 工程师;二是在公司里管着几百上千台服务器、想摆脱“每台机器都长得不一样”困境的运维和 SRE 从业者;三是对安全启动、TPM、磁盘加密这些底层机制好奇,想看看一套现代服务器系统应该怎么设计的开发者。我会从项目本身讲起,再逐个拆解它用的技术栈,最后聊聊我实际折腾这些组件时踩过的坑。

2. 为什么 Cloudflare 要折腾一套自己的发行版

很多人会觉得,Cloudflare 直接用 Ubuntu、Debian、CentOS 不就行了,为什么还要自己搞一套?这个问题问到点子上了。边缘网络和普通服务器集群有几个不太一样的约束,这些约束决定了它不能完全照搬通用发行版。

2.1 边缘节点本质上是一种“可替换硬件”

Cloudflare 在世界各地有大量边缘节点,每一台节点承担的任务高度一致:终止连接、缓存内容、执行过滤规则、上报指标。这些机器不是给某个业务团队单独使用的开发机,而更像是“接入网络的一部分”。对边缘节点来说,单台机器坏了不重要,重要的是整个节点网络仍然保持同构、同版本、同配置。

通用发行版默认是“一个人一台机”的思路。安装完系统、登录上去、手动改配置、跑起来,这套流程在管理几台服务器时很自然,但到了上千台节点,就是灾难。你会遇到各种“幽灵差异”:有的机器内核版本不一样、有的磁盘分区格式不一样、有的/etc下多了个莫名其妙的老配置文件。这些差异平时不显眼,出问题的时候却能让你排查到怀疑人生。

所以 Cloudflare 需要的不是一个“更好用的 Linux”,而是一套“生成 Linux 的流水线”。这台机器的系统是什么样,不取决于装了哪个版本的光盘镜像,而取决于仓库里某一时刻的代码。任何一台新机器,只要按照同一份描述去构建、去部署,它拿到的东西应当和其他机器没有二致。这不叫“装系统”,这叫“编译系统”。

2.2 NixOS 恰好满足了“整机即函数”的诉求

NixOS 对系统的建模方式和传统发行版完全不同。传统发行版想的是:我有一堆软件包,我把它们安装到固定目录,然后告诉系统服务怎么启动。NixOS 想的是:整个系统是一个由 Nix 表达式推导出的结果,/nix/store 里每个路径都有一个基于输入内容计算的哈希。只要输入推导式完全一致,产出的系统结构就完全一致。

这种“整机即函数”的模型对 Cloudflare 这类场景极其合适:

  • 服务器是什么状态,可以用代码写清楚,提交到 Git 仓库走评审流程。
  • 镜像构建是纯函数过程,同一份 flake.lock 在任何地方构建都会得到相同的软件包集合。
  • 系统升级不是“覆盖文件”,而是切换到新的/nix/store闭包,旧版本不会真的被删掉,可以随时回滚。
  • 机器状态可以被“垃圾回收”,不用像传统发行版那样担心东一榔头西一棒子装了一堆没有依赖关系管理的东西。

有人可能会问:Kubernetes 容器镜像不也能做到不可变部署吗?是的,但 Kubernetes 解决的是“应用层”的不可变,下面这一层操作系统还是需要有人管。你可以在容器里跑任意版本的应用,但宿主机内核、systemd、网络栈、磁盘加密、启动加载器这些“最后一公里”,依然需要一套方案来管理。cloudflare-os 的定位恰恰就是这最后一公里。

2.3 为什么没有直接从零写一个发行版

从零写发行版是个巨大的工程黑洞。你要维护软件包仓库、安全公告跟踪、构建基础设施、ABI 兼容测试,这不是 Cloudflare 的核心业务,投入产出比极低。NixOS 已经解决了大部分问题:它有 nixpkgs,有 active 的社区,有 flake 机制可以锁定版本,有 disko 可以把磁盘分区也变成声明式。

所以 cloudflare-os 更像是站在 NixOS 肩膀上做“剪裁与强化”。它的核心工作不是把 Linux 重新发明一遍,而是把 Cloudflare 对操作系统的诉求转化成一堆 Nix 模块,再把这些模块开源出来。这样做既能让 Cloudflare 内部用上统一操作系统,又能让外部看到 Cloudflare 认为一个服务器操作系统应该长什么样。

3. 从 flake.nix 到可启动镜像:核心链路拆解

下面这部分我尽量按照一条“从源码到开机”的链路来讲。你需要先有个概念:在 NixOS 的世界里,系统不再用“安装”这个词,而是用“构建”。你构建的是一个包含内核、initrd、systemd、所有服务配置的 bootable closure,然后把它放到目标机器的磁盘上。

3.1 入口文件 flake.nix:整个机房的出发点

项目根目录下会有flake.nix,这是 Nix flake 的入口。flake 你可以理解成 Nix 世界的“项目清单”,它声明了项目依赖哪些上游输入、对外提供哪些包和配置产物。

一个典型的 cloudflare-os 风格 flake 大致长这样:

{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; disko.url = "github:nix-community/disko"; lanzaboote.url = "github:nix-community/lanzaboote"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, disko, lanzaboote, ... } @ inputs: { nixosConfigurations = { default = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./configuration.nix disko.nixosModules.disko lanzaboote.nixosModules.lanzaboote ]; }; }; }; }

这个文件有几层意思。第一层,它把上游依赖锁定到具体 commit,而不是“最新版”,从而保证复现性。第二层,nixosConfigurations.default表示这个项目最终会生成一套完整的 NixOS 系统配置,后续所有模块都会注入其中。第三层,disko 和 lanzaboote 这两个模块负责的分别是“磁盘分区”和“安全启动”,它们不是可选项,而是这套系统的地基。

3.2 disko:把“磁盘长什么样”也写进代码

传统安装系统的第一步是分区。这个步骤在过去往往是手动完成,或者写一个 shell 脚本跑一遍,但脚本和真实系统的偏差很难追踪。disko 解决了这个问题:它允许你用声明式配置描述磁盘分区、加密、文件系统,然后直接生成对应的分区操作和挂载配置。

这里是一个高度简化的 disko 配置示例,展示它如何描述一块磁盘:

{ disk = { main = { type = "disk"; device = "/dev/sda"; content = { type = "gpt"; partitions = [ { name = "ESP"; start = "1M"; end = "512M"; bootable = true; content = { type = "filesystem"; format = "vfat"; mountpoint = "/boot"; }; } { name = "root"; start = "512M"; end = "100%"; content = { type = "luks"; name = "cryptroot"; settings = { allowDiscards = true; }; content = { type = "btrfs"; mountpoint = "/"; }; }; } ]; }; }; }; }

你可以看到,分区表、文件系统、挂载点、LUKS 加密全都写在一个 Nix 表达式里。这台机器如果系统盘坏了,拿同样配置重新生成一遍,出来的就是一台和原来一模一样的机器。这就是“磁盘即代码”的想法。

实际执行时,disko 会调用parted、cryptsetup、mkfs这些底层命令。所以它不是替代这些工具,而是给这些工具加了一层声明式控制。这样做的好处是,你不需要在每次部署新机器时回忆“上次我们是怎么分区的”;坏处也很明显,磁盘设备名可能在不同硬件上不一样,因此在大规模部署中通常还需要配合硬件识别策略,在配置里用by-path或by-id来定位设备,而不是硬编码/dev/sda。

3.3 bootspec 与 lanzaboote:把 Secure Boot 变成默认项

服务器操作系统一个很容易被忽略但很重要的环节是启动链路。大多数 Linux 发行版默认的启动方式是:

BIOS/UEFI → 引导加载器 → 内核 → initrd → 根文件系统

这里每一个环节都可能被篡改。物理接触过服务器的人,或者关心供应链安全的人,应该明白我在说什么:一台服务器如果整块硬盘可以被拔走,那上面的数据基本等于裸奔;启动引导器如果被替换成恶意版本,那整个系统都可能被控制在攻击者手里。

传统做法是在 BIOS 里设个密码、用 UEFI Secure Boot 检查引导加载器签名。但这些步骤往往是在系统装完之后手动 config 的,时间一长就没人记得。cloudflare-os 的思路是:安全启动不是后补的选项,而是系统构建的一部分。

这里就涉及两个关键组件:

  • bootspec:它定义了一套标准格式,描述“当前系统的启动条目是什么、内核在哪、initrd 在哪、启动参数是什么”。以前每个引导器都有自己的配置格式(比如 grub.cfg),bootspec 把这些信息统一成一个标准 JSON 结构。
  • lanzaboote:这是一个让 NixOS 原生支持 Secure Boot 的方案,它会把内核、initrd、signed systemd-boot 一起打包成可验证的启动闭包。

用 lanzaboote 时,配置大概长这样:

{ boot = { lanzaboote = { enable = true; pkiBundle = "/etc/secureboot"; }; bootspec.enable = true; }; }

其中pkiBundle是存放 UEFI 密钥和证书的目录。构建系统会使用这里的密钥对 bootloader、内核、initrd 做签名,最终生成一个只包含签名二进制文件的启动镜像。这样一来,哪怕有人物理接触机器、试图往 /boot 里塞恶意内核,Secure Boot 也会在 UEFI 层直接拒绝加载。

这个设计放在 Cloudflare 的场景里尤其合理:边缘机器分布在世界各地,不可能每个机房都安排一个安全员去盯着硬件被人动过没有,所以必须把“验证启动完整性的能力”内置到系统的构建逻辑里。

3.4 systemd 服务编排:用户态的部分也全部声明式

启动链路拉起来之后,接下来就是用户态服务。cloudflare-os 里所有东西都跑在 systemd 上,但服务的定义方式不是手写 unit 文件,而是通过 NixOS 的systemd.services模块来描述。

{ systemd.services.my-service = { description = "Example service"; after = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${pkgs.my-service}/bin/my-service"; Restart = "on-failure"; DynamicUser = true; }; }; }

你可能会说,这不就是用文本文件定义一个 systemd service 吗?区别在于,在 NixOS 里,这个 service 定义是系统一部分,它和内核、initrd、磁盘配置都在同一个构建闭包里。你对配置做的任何修改,都会被 Nix 精确追踪,回滚时也会连同服务定义一起回滚。

对各 region 差异比较大的机房,NixOS 还支持模块参数化。你可以定义一份 base 配置统一所有机器,然后按机房、角色引入不同模块。不需要写一堆 Ansible playbook 去“修补差异”,因为差异本身已经被建模成配置参数了。

4. 从零跑一个这类系统:我实际踩过的几个关键流程

光讲项目层面可能还是太抽象,我结合自己折腾 NixOS 这类发行版的经历,聊聊如果要真正跑起来,你会碰到哪些具体步骤和坑。

4.1 镜像构建命令

如果你在本地有 Nix 环境,可以把 cloudflare-os 这类仓库拉下来:

nix flake lock nix build .#nixosConfigurations.default.config.system.build.toplevel

这个命令会构建出完整的 system closure。构建完成后,产物会在result/里,里面包含内核、initrd、systemd 启动文件和整套/nix/store依赖。你可以把这个result复制到目标机器,然后在启动管理器里手动切换到它。更常见的做法是生成一个可安装镜像:

nix build .#nixosConfigurations.default.config.system.build.diskoImages

配合 disko 的镜像输出模块,你能得到可以直接写入磁盘的 raw 镜像,或者 ISO。拿它去给裸机装系统,整个流程会快很多。

4.2 第一次进系统后并没有那么“丝滑”

如果你真的在裸机上部署,会发现几个和传统发行版很不一样的点。

第一个是 rootfs 挂载。LUKS 加密分区在开机时会要求输入密码。如果你希望无人值守部署,就得用 TPM 2.0 绑定密钥,让系统在硬件完整性校验通过后自动解锁磁盘。这个功能在 NixOS 里由systemd-cryptenroll和 TPM 相关模块支持。我在配置时遇到过一个问题:不同主板的 TPM 驱动加载顺序不一样,有的机器 UEFI 固件要求先加载tpm-crb模块,否则 TPM 设备不出现,结果开机卡在内核阶段。解决办法是把tpm-crb加进 initrd 的boot.initrd.availableKernelModules,并且加一条initrd.supportedFilesystems声明。

第二个是 Secure Boot 的密钥管理。lanzaboote 需要你提供 Platform Key(PK)和 Key Exchange Key(KEK),以及用于签名的 Machine Owner Key(MOK)。这些密钥生成之后要小心保存,一旦丢失,已经部署的机器将来升级固件或系统时可能无法通过验证。我习惯把私钥放在离线签名机或专门的密钥管理系统里,不让它出现在构建服务器上。

第三个是网络初始化。如果机器没有 DHCP 保留地址,开机后你无法立即通过 SSH 连接。NixOS 的networking模块可以写成完全声明式的,但调试阶段总有网络没起来的时候。此时你会很怀念“一台机器一个控制台”的日子。

4.3 大规模更新:要有金丝雀机制

cloudflare-os 这类设计最舒服的一点是系统更新变成了“更换闭包”而不是“原地打补丁”。但你千万别直接在全部生产机器上同时nixos-rebuild switch。

原因有两个。第一,即使配置完全相同,硬件差异也可能引发启动问题;第二,一旦新闭包有问题,全量切换意味着所有机器同时挂掉。正确做法是维护一个金丝雀组:先部署一两台机器,观察指标、日志、错误率,正常之后再把同一闭包推到其他机器。NixOS 的好处是,因为闭包哈希一致,你可以先用金丝雀验证“这个哈希是否安全”,再在其他机器上放心部署同一哈希,而不是像传统方式那样每台机器都重新安装一遍。

5. 为什么这个项目最终走向“存档”:我的判断与收获

如果你上网去看 cloudflare-os 仓库,会发现它目前基本处于实验/停更状态。有人可能会因此说:看吧,NixOS 这套方案还是不行。我不太同意这种结论。

5.1 一个公开实验的使命不是变成长期产品

从 Cloudflare 的角度看,这个项目更像是一个“验证机”。它要验证的不是“能不能造发行版”,而是“用 NixOS 组织一个现代安全服务器系统是否可行”。在这种实验里,最重要的是把流程跑通,把知识沉淀下来,而不是维护一个长期向公众发布版本的 Linux。

事实上,这个仓库留下了几样很有价值的东西:

  • 一套完整的使用 bootspec、disko、lanzaboote 的参考配置;
  • 对 Secure Boot 下 NixOS 启动链路的一个可运行示范;
  • 一个“操作系统也是代码”的思路示范。

这些足够后来的工程师少踩很多坑。

5.2 对普通团队而言,可以先把这套方法论拆成更低成本的步骤

如果你现在管理几十台服务器,但不想引入全套 NixOS,可以先做几步迁移:

  1. 把服务器的基线配置用 Git 管理起来,任何改动必须通过 PR 合并,而不是登录服务器直接改。
  2. 用 PXE 或者云厂商的启动模板统一系统的初始镜像,避免手工安装造成的差异。
  3. 引入容器或 systemd 的服务单元模板,把应用层的不可变做到位。
  4. 等团队熟悉了声明式管理之后,再考虑把 NixOS 引入到新项目里。

这样做的目的不是一步到位,而是让运维团队从“我们是靠人记着每台机器长什么样”过渡到“我们是靠代码推导每台机器长什么样”。

5.3 如果团队想直接上 NixOS,这几个点别偷懒

根据我自己的实践,下面几件事在团队引入 NixOS 时属于“看起来麻烦、不做必炸”的范畴:

  • 必须锁定flake.lock。不锁定的话,一个月后重新构建可能会拉到完全不同的 nixpkgs 版本,复现性就无从谈起。
  • 密钥不进 Git。用sops-nix或agenix管理 secret,私钥用年龄(age)或 GPG 加密后放在仓库,明文只在构建时解密。
  • 构建产物要签名。如果有多台机器同步升级,必须让它们只接受带有可信签名的闭包,否则即使你构建了正确镜像,传输过程也可能被替换。
  • 先把回滚流程做成自动化。NixOS 的经典优势是回滚,但如果你只能手动在控制台敲命令回滚,那这个优势等于没有。

6. 深入一点:可复现构建之外,这套模式还带来了什么

最后我想说一个可能被忽略的价值层面。cloudflare-os 代表的其实不是“一家公司做了一套 NixOS 配置”这么简单,而是把“操作系统”从一个运行时的、需要持续照顾的状态,变成了可以进 CI/CD 流水线的构建产物。

一旦操作系统变成了构建产物,很多以前非常难做的事情会变得自然:

  • 你可以对系统做代码评审,而不仅仅是“看看配置文件对不对”。
  • 你可以给特定版本的系统打标签,精确知道生产环境跑的是哪个 commit 构建出来的东西。
  • 你可以把系统的单元测试放到构建阶段,比如在 QEMU 虚拟机里启动镜像,验证服务端口是否监听、安全启动是否生效,然后再决定是否推广。

这些能力在传统发行版运维模式下很难获得。不是说传统模式做不到,而是默认情况下没人会去做,因为这些步骤太多、太散、太容易遗漏。NixOS 把整条链路缩成了几个nix build和nixos-rebuild,这会让团队的工程化水平整体上一个台阶。

我自己的体会是:你不需要成为 Cloudflare 才用得上这套思路。哪怕只是把自己的个人服务器做成声明式配置,一年后回头看,你都会感谢当初把“系统状态”写进代码的决定。系统不再是某个夜晚手工折腾出来的黑盒,而是你随时可以重建、回溯、托付给自动化流程的确定性产物。

如果你打算尝试,建议从一台闲置机器或者一个虚拟机开始,先跑通 disko 分区、lanzaboote 安全启动、systemd 服务编排这三件事,然后再扩展到更多机器。等这套链路稳定了,你自然就会明白 cloudflare-os 的初衷——它不是要做一个“人人都在用的发行版”,而是要证明一件事:操作系统可以被认真严肃地当作工程产物来对待。