KVM 虚拟机模板制作与批量克隆:cloud-init 标准化交付

在 KVM 平台上批量交付虚拟机时,最耗时间的不是创建虚拟机本身,而是装完系统后逐台改主机名、配 IP、加用户、塞密钥。这类重复劳动的根源是没有一个“干净的模板 + 首次启动自动配置”的机制。本文用 cloud-init 补上这块:先把一台虚拟机收拾成不带任何机器身份信息的模板,再通过 virt-clone 或 qcow2 增量磁盘克隆出新机,新机首次开机时由 cloud-init 按每台不同的元数据注入主机名、用户、SSH 密钥和网络配置,做到克隆完即可登录使用。

适用环境:宿主机为 Rocky Linux 9 / CentOS Stream 9(Ubuntu 22.04 宿主机命令基本一致),libvirt + qemu-kvm,客户机为 Rocky Linux 9 或 Ubuntu 22.04 等带 cloud-init 的发行版。文中 IP 均为文档示例段 192.0.2.0/24,主机名为 vm-01、vm-02 等占位名,请按实际环境替换。

前置条件

  • 宿主机已安装并启动虚拟化组件:qemu-kvm、libvirt、virt-install、virt-clone,libvirtd 服务处于 running 状态。
  • 已准备 libguestfs 工具集(virt-customize、virt-sysprep 所在包),用于离线修改镜像。
  • 已有一台能正常启动的“母机”虚拟机,或发行版官方 cloud 镜像(qcow2 格式)。
  • 规划好克隆机的 IP 与主机名清单,后文以 vm-01(192.0.2.11)、vm-02(192.0.2.12)为例。

第一步:准备基础镜像

如果从官方 cloud 镜像起步,先把镜像复制到镜像目录并扩容到目标规格;如果是自己 virt-install 装的母机,跳过本步直接看第二步。

cp Rocky-9-GenericCloud.latest.x86_64.qcow2 /var/lib/libvirt/images/base-rocky9.qcow2
qemu-img info /var/lib/libvirt/images/base-rocky9.qcow2
qemu-img resize /var/lib/libvirt/images/base-rocky9.qcow2 40G

把母机以这个磁盘启动后,在客户机内安装 cloud-init 和 qemu-guest-agent,并确认服务开机自启。Rocky 系用 dnf,Ubuntu 系把 dnf 换成 apt 即可:

dnf install -y cloud-init qemu-guest-agent
systemctl enable cloud-init cloud-init-local cloud-config cloud-final qemu-guest-agent

再检查 /etc/cloud/cloud.cfg:确认 preserve_hostname: false(允许 cloud-init 按元数据改主机名)、disable_root: true(禁止 root 直接登录,改走普通用户加 sudo),数据源列表里保留 NoCloud 和 ConfigDrive 等你实际要用的项即可,多余的数据源只会拖慢首次启动的探测时间。

第二步:sysprep 式收尾,把模板做“干净”

这是整篇最关键的一步。模板里若残留机器标识,克隆机就会出现主机名相同、DHCP 拿到同一个 IP、SSH 指纹重复等一串问题。先在开机的母机内执行 cloud-init 自带的清理:

cloud-init clean --logs --seed
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -s /etc/machine-id /var/lib/dbus/machine-id
rm -f /etc/ssh/ssh_host_*
rm -rf /var/log/* /tmp/* /var/tmp/*
> ~/.bash_history
shutdown -h now

逐项说明:cloud-init clean 删除 /var/lib/cloud 下的实例状态和种子目录,保证克隆机首次启动时 cloud-init 会重新完整执行;machine-id 截断为空文件后,systemd 会在下次开机生成新值,基于 machine-id 申请 DHCP 的客户端也就不会撞车;SSH 主机密钥删除后由 sshd 在首次启动时重新生成(若发行版没有自动生成逻辑,在 cloud.cfg 的 cloud_init_modules 里确认有 ssh 这一项)。

关机后也可以在宿主机上用 virt-sysprep 离线再补一遍,适合镜像来源不可控时兜底:

virt-sysprep -a /var/lib/libvirt/images/base-rocky9.qcow2 \
  --operations defaults,-ssh-userdir \
  --hostname template
chmod 444 /var/lib/libvirt/images/base-rocky9.qcow2

最后把模板磁盘设为只读并另存一份备份,防止误启动或误写入污染模板。至此模板完成,下面的克隆都不应再直接修改这块盘。

这里再多说一句为什么“清理标识”不能省。虚拟机的身份信息分散在好几处:machine-id 决定 systemd 和 DHCP 客户端向外报的身份,SSH 主机密钥决定别人眼中“这台机器是谁”,cloud-init 的实例状态决定首次启动配置还会不会再跑一遍,网卡配置里写死的 MAC 或静态地址则直接决定网络通不通。手工装机交付时这些都是逐台生成的所以不出问题,而克隆是整盘复制,等于把一个人的身份证复印给了所有人。模板收尾的本质,就是把这些“出生时才该生成”的东西全部推迟到每台克隆机第一次开机时再各自生成,这样克隆才只是复制软件环境,而不是复制身份。

第三步:克隆虚拟机(两种做法)

做法一:virt-clone 整体克隆

virt-clone 会复制虚拟机定义并做一份完整磁盘拷贝,简单直接,适合数量少、磁盘不大的场景:

virt-clone --original template-rocky9 --name vm-01 \
  --file /var/lib/libvirt/images/vm-01.qcow2 --check path_in_use=off

做法二:qcow2 增量(backing file)克隆

以模板为只读后端文件,每台克隆机只生成一个增量层,创建是秒级的,空间占用也小,适合批量交付:

qemu-img create -f qcow2 -F qcow2 \
  -b /var/lib/libvirt/images/base-rocky9.qcow2 \
  /var/lib/libvirt/images/vm-02.qcow2 40G

virt-install --name vm-02 --memory 4096 --vcpus 2 \
  --disk /var/lib/libvirt/images/vm-02.qcow2,format=qcow2 \
  --import --os-variant rocky9 --network bridge=br0 --noautoconsole

注意增量克隆的前提是模板盘此后绝不能被修改或删除,否则所有增量层一起损坏;模板升级时应生成新版本模板(如 base-rocky9-v2.qcow2),新老并行,而不是原地改旧模板。

两种做法怎么选,看交付规模和后续运维方式。virt-clone 的每台虚拟机都是自包含的完整副本,迁移、备份、删除互不影响,适合长期运行的生产虚拟机,代价是每台都要占满一份磁盘、克隆时间随磁盘大小增长。增量克隆胜在创建快、初期占用小,适合测试环境批量起几十台用完即删的机器;但它和模板是强绑定关系,模板一旦出问题全军覆没,且增量层写久了会越来越厚,性能也略打折扣。经验上,生命周期超过几个月的机器用完整克隆,短命的批量测试机用增量克隆,两者不要混用在同一批交付里,免得运维时记不清谁依赖谁。

第四步:用 cloud-init 在首次启动注入配置

NoCloud 是最常用的注入方式:给虚拟机挂一张卷标为 CIDATA 的小 ISO,里面放 user-data、meta-data 和 network-config 三个文件。先写这三个文件:

# user-data
#cloud-config
users:
  - name: ops
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyMaterialReplaceMe ops@example.com
package_update: false
runcmd:
  - systemctl enable --now qemu-guest-agent
# meta-data
instance-id: vm-01-20261108
local-hostname: vm-01
# network-config(静态 IP 示例,网卡名按实际改为 eth0/ens3)
version: 2
ethernets:
  ens3:
    dhcp4: false
    addresses: [192.0.2.11/24]
    gateway4: 192.0.2.1
    nameservers:
      addresses: [192.0.2.53]

生成种子 ISO 并挂到虚拟机上启动:

cloud-localds /var/lib/libvirt/images/vm-01-seed.iso user-data meta-data --network-config=network-config
virsh attach-disk vm-01 /var/lib/libvirt/images/vm-01-seed.iso sdb --type cdrom --config
virsh start vm-01

没有 cloud-localds 时用 genisoimage 手工打也行,关键是卷标必须是 CIDATA:genisoimage -output seed.iso -volid CIDATA -joliet -rock user-data meta-data network-config。每台克隆机一份独立的 meta-data(instance-id 必须各不相同,cloud-init 靠它判断“是不是一台新机器”)。使用 ConfigDrive(OpenStack 风格)或 virt-install 较新版本的 --cloud-init user-data=...,meta-data=... 参数同理,不再展开。

验证与克隆后检查清单

克隆机开机一两分钟后,按下面清单逐项核对,任何一项不对都先别交付:

  • cloud-init status 输出 status: done;若是 running,说明还没执行完,继续等。
  • hostnamectl 主机名与规划一致,且各台互不相同。
  • ip addr 地址、掩码、网关与清单一致;ping 192.0.2.1 通。
  • 用注入的密钥能以 ops 用户免密 SSH 登录,且 sudo 可用。
  • cat /etc/machine-id 非空且各台不同;ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub 指纹各台不同。
  • qemu-guest-agent 正常:宿主机执行 virsh qemu-agent-command vm-01 '{"execute":"guest-ping"}' 有返回。
  • 确认无误后在虚拟机内执行 cloud-init clean 之前不要卸载种子 ISO 的检查:种子盘可在确认后从定义里移除,避免后续重启被误判。

常见问题

克隆机 IP 或主机名冲突。十有八九是第二步没做干净:machine-id 没清空时,NetworkManager/systemd-networkd 会用相同标识去申请 DHCP,服务器便把同一个租约发给多台“同一身份”的机器。逐台核对 /etc/machine-id 与 /var/lib/cloud/instance 是否存在残留,模板阶段补做清理后重新克隆,不要在已冲突的克隆机上逐台手改——那只能救急,根源还在模板里。

还没等 cloud-init 跑完就登录,发现用户和密钥都没生效。cloud-init 的用户创建、软件包操作都在 cloud-final 阶段,开机后需要几十秒到几分钟。先执行 cloud-init status --wait 等它结束再判断;真失败了看 /var/log/cloud-init.log 和 cloud-init analyze blame,常见原因是种子 ISO 卷标不是 CIDATA、user-data 第一行少了 #cloud-config 导致没被识别,或 YAML 缩进错误。

克隆机 SSH 提示主机密钥与记录不一致。若克隆机继承了模板里的主机密钥,多台机器指纹相同,客户端会告警且互相干扰。这正是删除 /etc/ssh/ssh_host_* 的原因;若你的镜像删除后不自动重建,在 user-data 里加一条 runcmd:ssh-keygen -A 兜底。

增量克隆的虚拟机启动报 backing file 缺失。模板盘被移动或改名了。用 qemu-img info --backing-chain vm-02.qcow2 查它记录的后端路径,要么把模板放回原路径,要么用 qemu-img rebase 修正,不要直接编辑 qcow2 文件。

模板更新了补丁,已经在跑的克隆机要不要重做?不需要。模板只影响“之后克隆出来的新机器”,已交付的虚拟机该走正常补丁流程就走正常流程,两条线互不干扰。正确做法是把补丁打进模板生成新版本(如 v2),此后新克隆用新模板,老机器按既有运维节奏升级;不要为了追求一致性去批量重克隆生产机,那等于把简单的补丁升级变成一次全量重建,风险完全不成比例。

小结

模板化的要点只有两个

:模板里不留任何机器身份(machine-id、主机密钥、cloud-init 状态、日志与种子),克隆机的个性化全部交给 cloud-init 在首次启动按 meta-data 注入。virt-clone 胜在简单,qcow2 增量克隆胜在快和省空间,按批量大小选择即可。把第二步的收尾动作写成脚本并纳入镜像制作流程,后续每次更新模板都能一键复现,交付质量才稳定。

发表回复

后才能评论