Incus 实战SWES · 技术记录

从裸机到 6 台小鸡:我的 Incus 一键部署脚本全解析

逐段解析 Incus 一键部署脚本:Zabbly 指纹校验、btrfs 配额池、proxy 代替 nat、随机密码与断点续跑设计,以及 9 项自动验收的取舍。

手动 incus launch 一台小鸡不难,难的是重复 6 次还不出错:装 Incus、建存储池、写 profile、装 SSH、配端口映射、设密码——任何一步手滑,后面全是坑。所以我写了 incus_3vps_docker_compatible_verified.sh(6 台版同源,只是 COUNT=6)。这篇逐段讲脚本,重点是每一行为什么这么写。

为什么这么做:可重复、可断点续跑

脚本的设计目标就三条:全新 Debian 12 上一遍跑通;网络中断、apt 卡住时重跑能接着干,不删已有容器、不重置已生成的密码;跑完自己验收,输出 PASS/FAIL,不靠人眼检查。

怎么做:逐段拆解

第一段:预检,拒绝在错误的环境上跑

[[ "$(id -u)" == 0 ]] || fail '必须以 root 运行'
[[ "${VERSION_CODENAME:-}" == 'bookworm' ]] || fail '仅适用于 Debian 12 bookworm'
[[ "$(dpkg --print-architecture)" == 'amd64' ]] || fail '仅适用于 amd64 架构'

Incus 的存储、网络操作全要 root;Zabbly 源只有 bookworm 分支;btrfs 镜像是 amd64 的。与其跑到一半报错,不如开头就拦。

第二段:Zabbly 源 + 指纹校验(上一篇讲过动机,这里直接给完整写法)

cat > /etc/apt/sources.list.d/zabbly-incus.sources <<'SRC'
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/lts-6.0
Suites: bookworm
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/zabbly.asc
SRC

用的是 DEB822 格式的 .sources 文件,Signed-By 明确绑定 key,不污染全局信任。

第三段:初始化与存储

if ! incus storage list --format csv 2>/dev/null | grep -q '^default,'; then
  incus admin init --minimal
fi

init --minimal 只做最少的事:建 incusbr0 网桥和默认存储。不搞集群、不问交互,适合单机。if 包裹保证重跑时跳过——这就是断点续跑的第一个体现。

第四段:Docker 共存的转发修复(原理下篇详讲,这里看结构)

脚本把修复逻辑写成独立脚本 /usr/local/sbin/incus-docker-forward-fix.sh,再配一个 systemd oneshot 服务开机执行:

[Unit]
Description=Permit Incus outbound forwarding through Docker firewall
After=docker.service incus.service

为什么是 oneshot + 开机自启?因为 Docker 每次重启都会重建它的 iptables 规则,之前加的放行规则会丢。服务设了 After=docker.service,保证每次开机都在 Docker 之后补规则。

第五段:容器创建——proxy 模式为什么不用 nat=true

每台小鸡的核心配置就三行:

incus launch images:debian/12 "$NAME" -p tiny-vps \
  -c limits.cpu=1 -c limits.memory=256MiB -c boot.autostart=true

注意镜像源是 images:debian/12——LinuxContainers 官方镜像站。系统容器镜像和 Docker Hub 的应用镜像是两个体系:前者是完整操作系统用户态(含 systemd),后者是单个应用。别混了,混了就会问出"为什么这个镜像里没有 systemctl"。

SSH 端口映射用的是普通 proxy 设备,刻意不用 nat=true:

incus config device add "$NAME" "ssh${SSH_PORT}" proxy \
  listen="tcp:0.0.0.0:${SSH_PORT}" \
  connect="tcp:127.0.0.1:22"

为什么?nat=true 模式下 Incus 会用 iptables DNAT 实现转发,规则藏在系统里,ss 看不到监听,出问题难排查。而普通 proxy 模式是 Incus 自己在宿主机 0.0.0.0:2201 上起监听,ss -lnt 一目了然,行为可预测。代价是多占一点用户态转发的开销——SSH 场景下忽略不计。

第六段:随机密码与密码文件

PASS=$(openssl rand -hex 16)
printf 'root:%s\n' "$PASS" | incus exec "$NAME" -- chpasswd

32 位十六进制随机密码,umask 077 + chmod 0600 保证密码文件只有 root 可读。关键设计:密码只生成一次——重跑脚本时,已有记录的容器跳过密码生成,避免"我明明记得密码,怎么登不上了"的惨剧。

SSH 的配置写法也值得说:没有直接改 /etc/ssh/sshd_config,而是写 drop-in 文件:

mkdir -p /etc/ssh/sshd_config.d
printf "PermitRootLogin yes\nPasswordAuthentication yes\nKbdInteractiveAuthentication no\n" \
  > /etc/ssh/sshd_config.d/00-incus-root.conf
sshd -t && systemctl enable --now ssh && systemctl restart ssh

drop-in 的好处:不污染发行版的主配置文件,以后 Debian 升级 sshd 不会产生 .dpkg-dist 冲突;sshd -t 先校验语法再重启,配错了不会把自己锁在外面;KbdInteractiveAuthentication no 关掉键盘交互认证,只留密码认证一种方式,attack surface 小一点是一点。

第七段:9 项自动验收

跑完逐台检查:容器 RUNNING、ssh 服务 active、网关可达、外网 IPv4、DNS、TCP 443、SSH 代理配置、宿主机端口监听、根盘 3GiB 配额。输出示例:

---- vps1 (SSH 2201) ----
[PASS] vps1 RUNNING
[PASS] vps1 ssh 服务 active
...

有一个 FAIL 就整体退出码非零,但不回滚——验收是告诉你哪里有问题,不是替你做决定。

脚本的工程习惯:幂等、 fail-fast、最小权限

通读脚本会发现几个贯穿始终的习惯,值得单独拎出来:

1. set -Eeuo pipefail + fail() 函数:任何命令失败、任何变量未定义,脚本立刻退出,不带病往下跑。配合开头的三重预检(root/bookworm/amd64),保证"要么完整跑完,要么一行不跑"。

2. 幂等设计:所有"创建"操作都用 if ! ... show 包裹——存储池已存在就跳过、profile 已存在就跳过、容器已存在就只调配置。配合"密码只生成一次",脚本可以反复重跑,网络中断后接着干,不会删容器、不会重置密码。这就是"断点续装"的全部秘密:没有魔法,只是每个步骤都先问一句"做过了吗"。

3. 密码文件格式:/root/incus-vps-passwords.txt 是 Tab 分隔的四列——容器名、用户名、SSH 端口、密码,chmod 0600 只有 root 可读。验收清单和密码表都从这个文件 awk 出来,单一数据源,不会出现"终端显示的密码和文件里记的不一样"。

4. 验收为什么查 9 项:每一项都对应一个真实翻车点。查"网关可达"用 ping,查"外网"除了 ping 1.1.1.1 还要查 TCP 443——因为有些网络禁 ICMP,ping 不通不代表 TCP 不通(脚本注释里明确写了"PING失败不一定代表TCP不可用")。查 SSH 要同时查代理配置和宿主机监听:incus config device get 对了只证明配置写了,ss -lnt 有 LISTEN 才证明真在监听。配置≠生效,这个习惯是排障排出来的。

验收失败时的处理哲学:有 FAIL 时脚本退出码为 2,但不回滚、不删容器,只打印一句"FAIL 可能由 ICMP 限制或已有容器配置不同引起,请核查对应项目"。为什么?因为验收失败的原因太多:有的是真问题(SSH 没起来),有的是环境特性(目标网络禁 ping)。脚本的职责是"把问题指出来",而不是"替你决定删不删"。自动化的边界感,体现在知道什么时候该停手。跑完先看 FAIL 项,一项项手动确认——这是脚本作者(我)能给你的最大诚实。

踩坑与演进

  • 旧版残留清理:早期版本试过 nat-tcp/nat-udp 端口段设备和 nft 防火墙守卫,后来发现 proxy 更直接、DOCKER-USER 定向规则更克制。脚本里专门有一段自动拆除旧版残留(删 incus-vps-firewall.service、删 nft 表、删旧 NAT 设备),保证新旧版本混跑不打架。
  • 6.0.6 的兼容坑:incus config 子命令不支持 --format json,踩到过 unknown flag 报错,后续脚本统一改用 incus query /1.0/... 走 REST API(重装脚本里有体现)。

最终效果与事实边界

如实说:3 台版脚本已在 AndNode 年付机真机跑过,vps1 成功 launch,SSH 端口映射、存储池、profile 全部就绪,验收项通过。6 台版与 3 台版同源(仅 COUNT 不同),以你实际运行结果为准,跑完请核对 PASS/FAIL 清单。

下一篇:两次真实排障——Docker 把小鸡的网掐了,以及 apt 撞上 Fastly 超时。

感谢阅读 · 保持好奇,认真记录。浏览更多文章 ↗