openrig:用声明式配置实现多设备初始化与运维自动化

发布时间:2026/11/2 14:06:38
openrig:用声明式配置实现多设备初始化与运维自动化

1. openrig是什么:从一个实际问题开始的定位

先说我为什么注意到这个项目。家里和办公室加起来有五台常开的机器:一台当NAS的迷你主机、一台跑容器集群的旧工作站、一台装了显卡的深度学习机器,外加两块树莓派和一块闲置的NUC。每台机器装系统的方式都不一样,有的用U盘手动装的,有的用PXE引导,有的干脆是从旧机器上直接把硬盘拆过来用的。时间一长,问题就来了:哪台机器上是什么内核、什么分区布局、哪些服务开机自启,我根本说不清楚,更别提每台机器重装一次要花掉的半天时间。

openrig这个工具,我是在一个讨论无人值守安装的英文社区帖子里翻到的。它解决的正是这个“多设备环境管理”的痛点。简单说,它是一套用声明式配置文件描述一台物理设备的目标状态,然后自动生成安装和初始化脚本并执行的工具。你可以把它理解成“把电脑环境写进YAML文件,然后让工具照着这个文件把你的机器变成你想要的样子”。

它适合谁?如果你手里超过一台需要反复初始化、重装、复现环境的机器,或者你想让“给新机器装系统”这件事从“手工操作”变成“执行一条命令”,那么openrig的思路值得看完。它尤其适合个人开发者、极客玩家、小型实验室维护者,因为它不像企业级的Cobbler或Foreman那样重,也不像Ansible那样需要先有一台能联网的现成机器才能开始工作。

我不太喜欢一上来就贴架构图或者功能清单,先讲清楚它的核心思想更好。openrig的整个工作流可以压缩成四个字:定义、生核、执行、回读。你在一个叫作 device.yaml 的文件里写下这台机器该有什么,openrig会先解析这份配置做合法性校验,然后生成一堆针对当前系统(目前优先支持Debian系和Fedora系)的配置脚本,接着在目标机器上执行,最后把执行结果汇总成一份报告。整个过程中,最关键的哲学是:你的机器状态应该是“可描述的”,而不是“不可复盘的”。手工装系统最大的问题就是不可复盘——装完即忘,坏了只能重来。openrig的出发点就是改变这一点。

一开始我也怀疑,这玩意儿跟Ansible有什么区别?跟Docker Compose又有什么区别?后面我会专门讲它们之间的边界。先接着往下看,看看它到底怎么运作的。

1.1 为什么需要openrig:从手工配置的痛点说起

手工配一台机器看起来不复杂,其实隐藏的痛苦非常多。举几个我自己的真实经历:

  • 给一台旧工作站重装系统,装完后才想起来“哎,我上次改过 /etc/security/limits.conf 里的文件句柄限制”,但具体改了什么,已经记不清了。
  • 一台跑KVM的宿主机,引导参数里加了intel_iommu=on,但换硬盘重装后忘了加,结果PCI直通怎么都过不了,排查了整整两天。
  • 树莓派上的SD卡损坏,换了一张新卡,从烧录系统到配置Wi-Fi、SSH密钥、时区、ZRAM、看门狗,全部重来一遍,每次至少四十分钟。

这些问题都不是“会不会装系统”的问题,而是状态管理的问题。openrig想做的,就是让你把这些零零碎碎的知识沉淀成一个文本文件。配置就是你的文档,文档就是你的配置。不需要额外再建一套Wiki去记录,这台机器怎么配的,看device.yaml就知道了。

我以前也试过用Ansible来管理,但Ansible的思路是“对一台已存在的机器持续操作”,它的Playbook写得再好,也绕不开一个鸡生蛋的问题:这台机器上要装的Ansible、要用的Python环境,总得有人先装好。openrig对这个问题的处理方式很有意思,它生成的不只是配置,而是能直接执行的环境初始化脚本,目标机器甚至可以是从没接入过网络的裸机(前提是你有办法把脚本送上去,比如通过U盘或PXE启动一个最小Live环境)。

1.2 openrig与相关工具的定位差异对比

为了让你更清楚openrig的适用范围,我画一个对比表,把几个容易混淆的东西放在一起看:

工具/方案核心工作方式最擅长解决的问题不擅长的事
openrig声明式定义设备状态,生成初始化脚本从零初始化一台机器、重装系统后快速恢复环境对存量系统的长期持续变更管理
Ansible基于SSH的即席命令执行和状态收敛对一批已经在运行的机器做批量配置从裸机到可用系统的完整初始化
Docker Compose容器级声明式编排应用层服务的依赖、端口、卷管理宿主机内核参数、磁盘分区、引导配置
PXE + Kickstart网络启动自动安装系统批量装机、分区、安装系统装完系统之后的应用层定制
NixOS全局声明式系统配置整个操作系统包括内核和软件包都由配置生成对非NixOS发行版的存量机器管理

从这个表你们能看到,openrig的生态位其实正好卡在“PXE装完系统之后”和“Ansible开始工作之前”这个区间里。它不管你怎么把安装介质跑起来,它管的是:系统起来之后,怎么快速变成你期望的状态,包括网络、存储、用户、软件包、内核参数、服务单元。

对比不是为了分高下,而是为了接下来的章节,你可以带着“我到底该用哪个工具”这个问题,去看openrig的配置到底能描述什么。

2. 下载安装与初始化工作区:十分钟跑通第一版

openrig的安装方式目前比较简单,它提供一个静态编译的二进制文件,直接下载丢进/usr/local/bin就能跑。也支持从源码构建,但我不太推荐普通用户从源码折腾,因为它的依赖里有几个跟系统引导相关的库,编译时容易因为缺少头文件失败。除非你要改它源码,否则直接用发布页的二进制就行。

安装完之后,第一件事不是写配置,而是初始化工作区。openrig init 命令会在当前目录生成一个标准目录结构。我个人习惯把它放在一个专门的地方,比如~/rigs,然后把整个目录纳入git管理。这样做有几个好处,首先目录结构本身就是文档;其次每次改动配置都能回溯;最后在多台机器之间同步配置时,只需要git push/pull即可,不需要依赖任何中心化服务器。

初始化后的目录大概是下面这个样子:

~/rigs/ ├── devices/ │ └── .gitkeep ├── templates/ │ └── base.yaml ├── keys/ │ └── .gitkeep ├── scripts/ │ └── .gitkeep └── openrig.yaml

简单解释一下每个目录的含义:

  • devices/:存放每台机器具体的设备配置,一台机器一个文件,比如nas.yaml、gpu-box.yaml。
  • templates/:存放可复用的模板片段,比如通用的基础软件包列表、通用的系统调优参数。
  • keys/:存放需要注入到机器里的公钥文件。注意,私钥永远不要放在这里。
  • scripts/:存放一些openrig不能直接表达但你又确实需要的自定义脚本片段。
  • openrig.yaml:全局配置文件,定义了一些工具行为,比如默认的SSH端口、执行超时时间、日志级别。

初始化完成之后,确认一下版本是否正常:

openrig version openrig doctor

doctor子命令会检查当前环境是否满足运行条件,比如是否安装了rsync、是否有lsblk和blkid命令、是否以root或有sudo权限运行。这一步很容易被忽略,但它能帮你提前发现一大部分环境问题。

2.1 一份最小可用的设备配置长什么样

在写完整语法之前,先给一个最小可用的 device.yaml,让各位建立直观感受。以下是我用来初始化树莓派4B的一份配置,去掉了繁琐的注释:

device: name: pi4-01 arch: arm64 distro: debian version: "12" network: hostname: pi4-01 interfaces: - name: eth0 dhcp: false address: 192.168.50.45/24 gateway: 192.168.50.1 dns: [192.168.50.1, 1.1.1.1] storage: - mount: / source: /dev/mmcblk0p2 fs: ext4 - mount: /boot source: /dev/mmcblk0p1 fs: vfat users: - name: pi ssh_key: keys/pi_id_ed25519.pub sudo: true packages: install: - curl - htop - tree - docker.io systemd: services: - name: docker enabled: true state: running sysctl: vm.swappiness: 10 net.ipv4.ip_forward: 1

这份配置把一台树莓派的目标状态描述得明明白白:网络静态地址、根分区是ext4、创建用户并注入SSH公钥、安装Docker、把它设置成开机自启、调整内核参数。

执行起来就一条命令:

sudo openrig apply devices/pi4-01.yaml

执行结束以后,可以用openrig status devices/pi4-01.yaml查看这台机器实际状态与目标状态的差异。如果你把配置里的vm.swappiness改成20,再跑一次apply,它只会改动这一项,不会把整个系统重来一遍。这种“只做差异”的行为,实际上是openrig比较讨喜的地方。

3. 配置语法详解:设备定义的核心元素

现在进入正题,讲讲openrig的配置语法。它使用YAML格式,总体结构分成几个顶层区块:device、network、storage、users、packages、systemd、sysctl、files。我逐个展开讲,并指出一些容易踩坑的细节。

3.1 device与network:设备身份和网络识别

device区块描述的是机器自身的元信息。其中arch字段会影响后续的包管理和脚本生成逻辑。比如在arm64的机器上,openrig生成的Docker安装脚本会自动添加对应的GPG key和apt源;在amd64上则走另一套逻辑。distro和version决定用的是apt还是dnf,这直接影响包安装的成功率。

network区块有一点要特别注意:接口名。真实Linux系统里的接口名不一定叫eth0,很多机器上叫eno1、ens3、wlan0。以我自己的经验,最稳妥的做法是先在目标机器上执行ip link查看实际接口名,再填进配置。如果接口名写错,openrig在apply阶段可能不会直接报错,而是跳过网络配置,导致机器陷入一个没有IP地址的尴尬状态。这个坑我踩过一次,后面在踩坑章节会详细复盘。

另外,如果你配置了静态IP但没有写网关,openrig会直接拒绝执行。它的设计思路是:网络配置必须完整,不允许给出一半的状态定义。

对于有多个接口的机器,openrig支持按MAC地址匹配接口,而不是按名称。这在大规模装机时非常有用,因为同一型号的机器接口名不一定按相同顺序分配。你可以这样写:

network: interfaces: - mac: "a4:5e:60:xx:xx:01" dhcp: false address: 192.168.10.5/24 gateway: 192.168.10.1

按MAC匹配的好处在于:无论系统怎么重装,接口名怎么变,最终拿到的IP都是确定的。对跑着NFS、数据库这类依赖固定IP的服务来说,这一点很重要。

3.2 storage:磁盘分区和挂载点的边界

storage区块用来定义磁盘挂载关系。这里我不建议你让openrig自动格式化分区,因为它目前对分区的支持还比较谨慎,默认情况下只会做“确保挂载点和文件系统正确”的操作,不会主动破坏已有数据。

什么意思?也就是说,如果你在配置里写着/dev/sdb1挂载到/data,文件系统是ext4,那openrig的行为是:如果这个分区已经是ext4并且挂载信息符合要求,它就跳过;如果看起来不是ext4,它会在apply时给出一个 warning,让你确认是否要格式化。这是有意的设计——openrig不想默认成为一个“数据销毁工具”。

如果确实需要格式化,可以显式声明:

storage: - mount: /data source: /dev/sdb1 fs: ext4 force_format: true

force_format加上了,它才会执行真正的格式化。我的建议是,凡是有宝贵数据的分区,永远不要加这个字段。哪怕是新硬盘,也要再三确认盘符没有写错,因为/dev/sda和/dev/sdb在设备重启之后是有可能互换的。后面踩坑部分我会详细讲设备节点漂移怎么处理。

3.3 users与文件注入:密钥和配置文件怎么管理

users区块用于创建用户、设置shell、注入SSH公钥、是否加入sudo组。其中SSH公钥的路径是相对于工作区根目录的。比如你在keys/目录里放入pi_id_ed25519.pub,配置里就写keys/pi_id_ed25519.pub。openrig会把公钥内容读取出来,写入目标机器的~/.ssh/authorized_keys。

这里有个容易疏忽的点:openrig不会为注入的公钥自动设置权限。如果目标机器上的~/.ssh目录权限是777,sshd会拒绝加载公钥。所以我在配置里通常还会配合一个files片段来修正权限。

files区块做的事很简单:把工作区内的某个文件复制到目标机器的指定路径,并可设置权限和属主。比如:

files: - source: files/sshd_config dest: /etc/ssh/sshd_config mode: "600" owner: root group: root

再强调一次:放入工作区的敏感文件,如果工作区是git仓库,请务必确保已经用.gitignore排除掉。我之前看到有人直接把带私钥的目录推到公开仓库里,那个场面相当惨烈。

3.4 packages、systemd与sysctl:软件、服务和内核参数的声明式管理

packages区块的逻辑很直接:install列表里写要装什么包,openrig会调用当前发行版的包管理器来装。如果发现包已经安装,则不会重复操作。

systemd区块用来自动化处理服务的启停和开机自启。这个区块的价值在初始化和重装场景下特别大。你想一想,手工装完Docker之后还要记得systemctl enable --now docker,但如果你在配置里写了enabled: true和state: running,那从零到服务在线就只剩下一条apply命令。

sysctl区块用来设置内核参数。我建议把跟网络转发相关的、跟内存回收相关的、跟文件句柄限制相关的配置都放在这里统一管理。需要注意,有些内核参数写在/etc/sysctl.conf里需要执行sysctl --system才能生效,openrig会自动处理这一步,但有一个特例:如果你的参数涉及net.*但对应模块尚未加载,有可能会失败。这种情况下需要在配置里增加一个packages.install或systemd的方式先加载模块,或者用files往/etc/modules-load.d/里写一个模块加载配置文件。

4. 执行与验证:从dry-run到真正的apply

openrig的设计者显然很了解“直接改系统”有多危险,所以它在apply之前设置了一道dry-run关卡。刚开始用的时候我还不以为意,觉得多此一举,后来有一次我的配置里有个严重的网络错误,全靠dry-run才避免把一台远程机器变成离线状态。

4.1 dry-run到底做了什么

执行openrig apply devices/xxx.yaml --dry-run的时候,openrig不会在目标系统上做任何变更,但它会做以下几件事:

  1. 解析YAML语法,检查必填字段。
  2. 检查引用的文件是否存在,比如 keys 目录下的公钥、files 目录下的配置。
  3. 检查存储设备节点是否存在。如果配置里写的/dev/sdc1当前不存在,它会直接指出。
  4. 对网络配置做语义检查。比如明明填了静态IP,却没有默认网关,直接报错。
  5. 对比当前系统状态和目标状态,输出一份差异报告:哪些包会安装,哪些服务会启用,哪些内核参数会变化。
  6. 对安装包的操作做“影响预估”。比如它会告诉你,这个安装会让apt多占用多少磁盘空间(通过apt-get --simulate之类的方式)。

说白了,dry-run就是一个“预演”。它能帮你把绝大多数因为手误导致的问题拦截在真正执行之前。我现在的习惯是,任何配置文件修改完,都会先跑一次dry-run,确认差异符合预期,再执行真正的apply。没有意外,就不要跳过dry-run。

4.2 apply执行的分段策略和异常中断处理

dry-run通过之后,真正的apply动作会分几个阶段执行,顺序也是固定的:

  1. 网络配置阶段:主机名、接口IP、路由、DNS。
  2. 用户与文件阶段:创建用户、注入密钥、写入配置文件。
  3. 存储挂载阶段:处理文件系统、挂载点。
  4. 软件包阶段:更新软件源,安装指定包。
  5. 系统服务阶段:启用并启动systemd服务。
  6. 内核参数阶段:应用sysctl配置。

每个阶段执行前,openrig会生成一个快照,记录当前阶段的配置状态。这样做的目的是:如果某个阶段失败,openrig可以告诉你失败发生在哪一步,以及该步骤之前哪些已经完成。

有一个真实的场景能说明这个设计有多重要:有一次我给一台机器安装Docker,但网络源慢导致包安装超时,整个apply中断。openrig的日志显示网络配置阶段已完成,用户阶段已完成,存储挂载已跳过,软件包阶段失败。我只需要调整软件源镜像,重新执行apply,它就会跳过已完成的部分,继续安装未完成的包。这种“分段断点续跑”的机制在实际操作中省了我非常多的时间。

4.3 状态报告怎么读

apply结束之后,无论成功还是部分失败,openrig都会给出一份状态报告。我第一次用时没仔细看,结果漏了一个warning。后来总结经验,读取报告时我会盯着这几个字段:

  • changed:实际发生了变更的项目数。如果为0,说明系统已经处于目标状态。
  • pending:因为某些原因(比如被排除的项,或需要重启才能生效的项)尚未生效的变更。
  • warning:不阻断执行但需要留意的信息,比如某个内核参数需要重启后才生效。
  • error:失败的项目,以及失败原因。

如果你看到pending里面有“kernel parameters require reboot”之类的内容,那就意味着这台机器需要一次重启。openrig不会自作主张帮你重启,它把选择权留给你。我会专门在配置里写明哪些机器允许自动重启,到时候再单独处理。

5. 我踩过的坑和对应的解决办法

用openrig管理几台机器差不多两个月了,中间遇到过不少让人抓狂的问题。这一部分我挑几个最有代表性的,把排查过程写出来,希望其他人遇到类似情况时能直接跳过这些坑。

5.1 设备节点漂移:/dev/sda不一定是那块硬盘

有一次我配置了一台机器,打算把一块新加的SSD格式化成ext4并挂载到/srv。当时我在配置里写的是/dev/sdb1,dry-run也通过了。执行apply的时候,openrig提示格式化失败,我去看系统日志,发现/dev/sdb根本不存在。再执行lsblk一看,原本应该是空的sdb变成了原来的系统盘,而那块新SSD变成了sdc。

这就是Linux的设备节点漂移问题。设备名不是固定的,取决于内核发现设备的顺序,一旦你的机器有多个磁盘控制器、USB转SATA设备混在一起,盘符漂移几乎必然会遇到。

用openrig解决这个问题有几种方案:

  • 用/dev/disk/by-uuid/来指定分区,UUID在格式化之前就已经确定,且不会漂移。
  • 用/dev/disk/by-id/来指定磁盘,这个路径包含硬盘品牌和序列号,最可靠。
  • 用/dev/disk/by-label/,前提是你给文件系统设置了明确的label。

我最终在两个文件里全部改成了by-uuid或者by-id的写法。虽然配置看起来啰嗦一点,但再也没有出现过挂载错盘的事故。记住:在涉及格式化的操作里,设备节点漂移不是可能不会发生的问题,而是迟早会发生的问题。

5.2 静态IP配上去了,但DNS老出问题

第二次踩坑是在配置一台Ubuntu Server 22.04时,网络部分我给了静态IP和DNS,apply之后系统是能ping通网关了,但域名解析总间歇性失败。我手动看/etc/resolv.conf,发现它指向的是127.0.0.53,这是一个systemd-resolved管理的存根解析器。openrig在校验时看到我配置里的dns字段,认为只要把DNS写进系统网络配置就算完成,但实际上systemd-resolved会接管这个文件,openrig写的内容并没有真正生效。

这个问题在Debian系的新版本里特别普遍。处理办法也简单,要么在配置里显式关闭systemd-resolved服务,改用传统的/etc/resolv.conf管理方式;要么直接利用systemd-resolved的配置文件,把DNS设置写进/etc/systemd/resolved.conf再重启服务。

我倾向于后者,因为保留 systemd-resolved 对现代Ubuntu的Netplan管理有利。配置里大概是这个样子:

systemd: units: - name: systemd-resolved state: running enabled: true files: - source: files/resolved.conf dest: /etc/systemd/resolved.conf mode: "644" owner: root group: root

我之前装旧版Debian时根本没有这个问题,所以第一次在这个环境里跑openrig成功但DNS失效时,花了不少时间排查。这里提醒各位:凡是涉及DNS的配置,apply完以后务必实测一次resolvectl status和dig。

5.3 模板继承:多台机器不再重复造轮子

有一件事明显提升了我的使用体验,就是openrig的模板继承功能。一开始每台机器我都是从头写 device.yaml,结果发现每台机器几乎都要安装curl、htop、git,都要做相同的SSH加固,都要设置一样的时区。复制粘贴了三四次之后,我就开始看它的模板机制。

openrig的模板本质上还是YAML,但它支持引用其他文件,并做字段合并。比如我可以建一个templates/base.yaml:

template: name: base packages: install: - curl - git - htop - tree files: - source: files/sshd_config dest: /etc/ssh/sshd_config mode: "600" owner: root group: root sysctl: vm.swappiness: 10 net.ipv4.ip_forward: 1

然后设备配置里声明继承它:

device: name: gpu-box arch: amd64 distro: ubuntu version: "22.04" templates: - base network: hostname: gpu-box interfaces: - name: eno1 dhcp: false address: 192.168.8.15/24 gateway: 192.168.8.1 dns: [192.168.8.1] storage: - mount: / source: /dev/disk/by-uuid/xxxx fs: ext4 users: - name: dev ssh_key: keys/dev_id_ed25519.pub sudo: true packages: install: - nvidia-driver

openrig做合并的时候有个规则:具体设备配置中出现的字段会覆盖模板中的同名字段,列表字段默认是追加而不是替换。也就是说,模板里装了curlgithtop,设备配置里又装nvidia-driver,最终安装列表是四个包。如果你确实想从设备配置中排除模板里的某个包,可以显式写一个packages.exclude字段。这种语义很直观,不会产生“模板覆盖了整个列表”这种令人困惑的行为。

现在我的工作区里,模板越积越多:base.yaml是每台机器都会用的;nas.yaml加了存储相关工具;desktop.yaml加了图形界面相关包;gpu.yaml加了驱动支持和CUDA工具链。每来一台新机器,配置过程就从“写一整套环境”变成了“套模板+填差异”,大概十分钟就能完成一份可用的设备配置。

5.4 和Ansible、NixOS的协作边界

讲到这里,我觉得有必要回过头说说openrig和Ansible、NixOS这类工具到底怎么共存。这不是非此即彼的选择题。在我自己的环境里,它们各管一段:

  • openrig负责“从刚装完系统到基础可用”这一段。包括网络、用户、SSH、基础工具、Docker安装、内核参数。这段的事情,用openrig最顺手。
  • Ansible负责“持续运维”这一段。比如定期更新配置、分发新版本的应用配置、批量执行运维操作。Ansible的好处是可以从控制机直接对一批机器执行,不需要每台机器都先跑一次apply。
  • NixOS负责那些“希望整个系统都由配置文件生成”的机器。NixOS是一种更极致的声明式系统,它有自己的一套状态管理机制。openrig并不试图替代NixOS,两者面向的用户群体不完全一样。

所以我的建议是:如果你还在用传统发行版(Debian、Ubuntu、Fedora),并且厌倦了手工初始化机器,给openrig一个机会。它不要求你推翻现有的运维体系,只需要你拿出十分钟定义第一台机器,就能感受到“配置即状态”带来的踏实感。

我自己现在的流程已经稳定为:拿到新机器 → 制作启动U盘或快速装一个基础系统 → 把 openrig 二进制传上去 → 在~/rigs目录下写好对应的 device.yaml → 执行 apply。整个过程从之前的四十分钟变成十几分钟,其中大部分时间其实是等待包下载。之前那些“当时怎么配的来着”的困惑,现在变成了“打开配置文件看一眼”就能回答的问题。