Files
homelab-infra/infrastructure/kata-lxc-lab
panxiao81andClaude Opus 5.5 55bb5be8b6 归档:Kata / microVM runner 实验(2026-09-17 工作区快照)
从旧工作区 chore/recover-old-workspace 清理时保存,内容与 2026-09-17
stash@{0} 快照中的版本一致;未合并、未在 main 上使用,仅作参考,不开 PR。
被 gitignore 的 tfstate 与凭据文件不在此分支,仍留在本地工作区。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 17:30:48 +00:00
..

kata-lxc-lab

在 pve2 上验证 privileged LXC 内运行 k3s、Kata/QEMU,并直接使用 PVE host 的 /dev/kvm、/dev/vhost-net 与 /dev/vhost-vsock。这是可销毁 PoC,不承载持久数据。

Terraform 复用原 VM PoC 的 ignored credential 文件,不复制 token:

cd infrastructure/kata-lxc-lab/terraform
export SSL_CERT_FILE=/tmp/kata-lab-ddupan-ca.pem
terraform init
terraform plan \
  -var-file=../../kata-lab/terraform/credentials.auto.tfvars
terraform apply \
  -var-file=../../kata-lab/terraform/credentials.auto.tfvars

当前 PoC 使用 pve2、VMID 147、Ubuntu 24.04 LXC template 和 16 GiB local-lvm rootfs。容器为 privileged,并开启 nesting、keyctl、FUSE、mknod; Kata 所需的 KVM、vhost 与 /dev/net/tun 设备显式透传;另透传 /dev/kmsg,因为 kubelet 启动时会打开该设备。 PVE 9 的 force_rw_sys=1 feature 用于开放 kubelet 所需的少量 /proc/sys 写操作; provider 0.111.1 尚未暴露该 feature,因此同样由 pct 设置。

k3s/Kata worker 还使用以下 privileged LXC 原生配置;PVE 9 使用 cgroup v2,因此设备 规则采用 lxc.cgroup2.devices.allow:

lxc.apparmor.profile: unconfined
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.mount.auto: proc:rw sys:rw

这些设置让容器内 kubelet、containerd 和 Kata shim 能管理 mount、cgroup、设备与 共享宿主 sysctl。因此外层 LXC 不是安全边界;不可信 CI 的安全边界是内层 Kata VM。 /dev/kmsg 当前直接透传,没有使用 /dev/console symlink fallback。

PVE 将 privileged LXC 的创建限制为 root@pam(API token 即使拥有 PVEAdmin 也 缺少 root-only 的 Sys.Modify)。因此实际创建入口使用 root SSH 上的 Ansible/pct:

cd infrastructure/kata-lxc-lab/ansible
ansible-playbook create.yml
ansible-playbook bootstrap.yml
ansible-playbook kata.yml
ansible-playbook verify.yml
ansible-playbook verify-fuse.yml

Terraform 文件保留用于记录 provider 权限边界与声明式目标;当前 token 执行 apply 会 在创建前返回 403,不会产生资源或 state。

验证结果

当前管理地址为 192.168.10.128:2026-09-14 主 LAN DHCP 池调整后,从 .11 续租迁移到 .128,并同步 Ansible inventory。它仍是动态租约,不是固定地址。

2026-09-14,pve2 VMID 147 / 192.168.10.11 验证通过:

  • LXC 内 k3s v1.36.4+k3s1 node Ready;
  • Kata Containers 4.1.0 qemu-runtime-rs 安装成功;
  • Kata guest kernel 6.18.35,外层共享的 PVE kernel 7.0.2-6-pve;
  • Kata Pod 内的 Docker 27.5.1 daemon 成功运行 busybox:1.37 嵌套容器;
  • /dev/net/tun 是 Kata 创建 TAP/link 的必要设备,未透传时 runtime-rs 在 create link 返回 ENOENT;
  • fuse-overlayfs smoke test 成功:Docker 报告 storage-driver=fuse-overlayfs,并成功运行嵌套容器。

正式方案保持 Kata 默认 emptydir_mode = "shared-fs",不向 LXC 暴露 host /dev/loop-control 或 /dev/loopN。dockerd 镜像需要包含 fuse-overlayfs;启动前在 Kata guest 内创建临时设备节点 mknod /dev/fuse c 10 229,随后强制传入 --storage-driver=fuse-overlayfs。该 /dev/fuse 属于内层 guest kernel,不是 PVE host 设备透传,并随 Kata Pod 一起销毁。

删除三个 smoke Pod 后,LXC cgroup 统计约 505 MB anonymous memory、1.34 GB file cache;后者主要来自刚拉取的 Kata/Docker images,可由宿主回收。也就是说常驻 worker 的不可回收 working set 约 500 MB,8 GiB 配置是 cgroup 上限而非预分配。

Gitea ephemeral runner PoC

runner/ 定义一次性 Gitea runner Pod。整个 Pod 使用 runtimeClassName: kata; runner 通过 guest 内同 Pod 的 dockerd 创建 job container,不连接 LXC/PVE host 的 Docker socket。dockerd 使用 zot 中预先发布的 dind-fuse:29.7.1,其 /var/lib/docker 是随 Kata Pod 删除的 emptyDir。

runner 以 GITEA_RUNNER_EPHEMERAL=1 注册,只接收一个 runs-on: kata-poc job;接单 后 Gitea 撤销其 runner credential,job 完成后进程与 Kata VM 一起退出。当前 PoC 手动创建一个 runner 等待 .gitea/workflows/kata-runner-smoke.yml,尚未部署 workflow_job webhook controller,因此不会保留常驻空闲 runner。

注册 token 只在运行时从现有 OpenBao/ESO 消费副本投递到 PoC 集群的临时 Secret, 不写入此目录。两个 Pod image 使用 imagePullPolicy: Never,部署前从可信工作站将 已验证镜像导入 PoC k3s containerd;这是当前 zot 拉取仍要求 SPIFFE 身份时的 bootstrap 边界,不是正式镜像分发方案。

正式接入 zot 前需要把这个 cluster 注册到 SPIRE,并为 runner Pod 创建专用 ClusterSPIFFEID。届时 runner 与 dockerd sidecar 在同一路径只读挂载 CSI Workload API socket,runner 的 container.options 再把该路径 bind-mount 到 job container。 zot 只对最终 SPIFFE ID 的 panxiao81/dind-fuse 仓库授予 read/create/update;禁止授予 namespace 或整个 trust domain 写权限。

2026-09-14 已完成真实 Gitea job 验收:Actions run 242 / job 445 使用 alpine:3.22,10 秒成功;runner container 退出码为 0,原生 dockerd sidecar 随后 由 kubelet 终止,Pod 最终为 Succeeded,没有保留空闲 Kata VM。guest 内核为 6.18.35,dockerd 29.7.1 使用 fuse-overlayfs。

首次尝试暴露了两个部署陷阱:COPY 默认保留源文件 mode,入口脚本原为 0664, 必须在 Dockerfile 使用 COPY --chmod=0755;只配置 ephemeral-storage: 20Gi limit 会形成同值 request,使 12 GiB PoC 节点无法调度。当前 manifest 显式 request 1 GiB、 limit 8 GiB。完整 ubuntu-latest job image 也不适合这个小 rootfs,基础生命周期验收 改用 Alpine;正式 worker 必须扩容本地 image/cache 空间或使用更精简的 job image。

Cloud Hypervisor 内存记账验证

2026-09-17 在 pve2 的一次性 VMID 148 中安装 k3s v1.36.4+k3s1 与 Kata 4.1.0, 只启用 clh-runtime-rs,验证 Cloud Hypervisor 在 privileged LXC 内不会重现早期 microVM PoC 的 private memfd 双重记账问题。测试完成后已删除 VMID 148。

Cloud Hypervisor /api/v1/vm.info 报告:

{"size":3254779904,"shared":true,"hugepages":false,"prefault":false,"thp":true}

Kata guest 在 memory-backed emptyDir 中写入 1 GiB 后:

  • VMM Pss_Shmem 从约 247 MiB 增至约 1269 MiB;
  • VMM Pss_Anon 写入前后均约 1.2 MiB;
  • 外层 LXC shmem 增加约 1 GiB,anon 基本不变;
  • LXC memory.events 的 max、oom 与 oom_kill 均为 0;
  • 删除 Pod 后 VMM 退出,LXC shmem 回落到约 1.6 MiB。

因此 Kata 的 Cloud Hypervisor handler 已使用 shared memfd,等价于独立 launcher 的 --memory shared=on 修复;后续 OpenSandbox/Kata 方案无需为 guest RAM 预留近似双倍 的 LXC cgroup 内存。部署验收仍应检查 vm.info.config.memory.shared=true,防止上游 配置或 runtime handler 变化造成回归。

测试期间还发现 pve2 local-lvm thin pool 已达到 100%;临时 rootfs 必须放到 pve-rg-hdd,不能根据容器内部 df 的剩余空间判断 thin pool 是否可写。

Block-backed emptyDir 与 CI 构建验证

2026-09-17 又在 pve2 的一次性 VMID 148 中验证 Kata 4.1.0 clh-runtime-rs 的以下 drop-in:

[runtime]
emptydir_mode = "block-plain"

结果如下:

  • 普通 Kubernetes emptyDir 被创建为稀疏 disk.img,由 Cloud Hypervisor 直接作为 block device 热插拔;guest 内 /var/lib/docker 是 /dev/vdb 上的 ext4。
  • Docker 29.1.5 成功强制使用原生 overlay2,不再需要 fuse-overlayfs;整个过程 没有使用 PVE host 或 LXC 的 loop device。
  • emptyDir.sizeLimit: 8Gi 没有决定虚拟磁盘容量。由于 kubelet 所在文件系统约 12 GiB,guest 看到的 ext4 约 11.7 GiB;这与 Kata 已知的 block-backed emptyDir 限制一致。
  • 包含 256 MiB payload、2000 个小文件和一个额外复制层的冷 BuildKit 构建耗时 46 秒;同一 LXC、同一 DRBD HDD 存储上的 runc 对照为 34 秒,Kata 约慢 35%。
  • 在 overlay2 容器层内写入并 fsync 512 MiB:Kata 为 12 秒(40.5 MB/s),runc 为 10 秒(50.3 MB/s);创建一万个小文件两者均约 2 秒。
  • kind v0.27.0 / Kubernetes v1.32.2 首次启动失败是因为 Kata guest 内的 dockerd 容器没有 /dev/kmsg,不是 block storage 或 cgroup 故障。在 privileged dockerd 启动前执行 mknod /dev/kmsg c 1 11 后,kind 创建成功:总计 122 秒,进入等待阶段 后 23 秒 control-plane Ready,随后可正常删除。
  • 测试时稀疏 backing file 逻辑大小约 12 GiB,构建和 kind 后实际占用约 1.8 GiB。 删除 Pod 后 backing file、Cloud Hypervisor 进程和 kind 容器全部消失;host/LXC 均无 loop device 残留,LXC 没有 OOM 事件。

因此 block-plain 能解决 virtio-fs 不能作为 overlayfs upperdir 的问题,并满足 BuildKit 与 kind 的功能要求。正式 CI RuntimeClass 必须使用独立的 block-backed 配置,并在 dockerd bootstrap 中创建 guest /dev/kmsg。它仍比同机 runc 构建慢约 20–35%,且 emptyDir.sizeLimit 不能可靠限制虚拟磁盘容量。PoC 曾据此建议为 kubelet 目录提供独立文件系统;正式 sandbox 集群最终没有采用该设计:独立 volume 增加了 DRBD 与 LXC 挂载生命周期复杂度,却没有隔离 containerd 等其他节点数据。正式节点 改用单一、容量明确的 rootfs,并以磁盘占用监控、Pod 删除后的 backing-file 回收检查 和实际构建基准作为上线门槛。