---
url: /courses/virtualization-tech/07-clone-template-cloud-init/index.md
---
# 第 7 课 一分钟复制实验室：虚拟机克隆、黄金模板与 cloud-init 身份去重

::: tip 项目目标
在前面的课程中，你已经掌握了通过图形界面（virt-manager）与命令行工具（virsh / virt-install）独立安装和管理单台 Ubuntu Server 虚拟机。然而在企业级云计算平台、多节点分布式集群与自动化测试运维环境中，如果每台虚拟机都从 ISO 光盘镜像逐步点击安装，不仅重复耗费大量的计算时间与存储算力，而且极易导致各个节点环境配置漂移。
通过本课的实训与深入剖析，你将达成以下目标：

1. **掌握 virt-clone 完整克隆底层机制**：牢固掌握关机安全门禁，使用 `virt-clone` 执行自动化完整克隆，深入理解写时复制（COW）与 backing file 依赖链，并通过 `qemu-img info --backing-chain` 验证底层 QCOW2 磁盘 100% 独立无依赖；
2. **透视多重身份体系与三大冲突灾难**：理清宿主侧（Domain 名称、libvirt UUID、磁盘路径、MAC 地址）与来宾内部（hostname、`/etc/machine-id`、IP / Netplan）的边界，深度推导未经清理直接克隆导致的 Kubernetes 节点互踢、journald 日志污染与 DHCP DUID 抢占三大分布式灾难；
3. **拆解 cloud-init 四阶段流水线与黄金模板铁律**：剖析 cloud-init 四阶段执行流水线架构（Generator -> Network -> Config -> Final），在候选虚拟机内执行 `sudo cloud-init clean --machine-id` 彻底抹除实例运行痕迹与旧 ID，牢固树立黄金模板五项运维铁律；
4. **极速派生实例与 NoCloud 企业级自动化**：从黄金模板秒级派生目标虚拟机 `lab01`，掌握首次启动（First Boot）去泛化激活机制，了解 NoCloud `cidata.iso` 自动化注入方案，完成全套身份独立性验收。
   :::

![将配置完善的 Ubuntu Server 固化为黄金模板并批量派生身份独立的实验虚拟机](./assets/06/clone-template-illustration.png)

## 7.1 任务一：快速复制虚拟机——virt-clone 完整克隆实操

在日常运维中，当我们需要多台环境完全一致的服务器时，最直接的方式就是复制已经配置完善的基准机。在 KVM/libvirt 体系中，`virt-clone` 是专门用于克隆虚拟机的核心工具。

::: tip 操作环境与前置准备

* **操作终端**：本任务的所有命令默认在 **Ubuntu 宿主机终端（Host）** 执行，请勿在虚拟机内部执行。
* **管理权限**：命令默认连接 `qemu:///system` 系统级实例。如果普通用户执行提示权限不足，请在命令前添加 `sudo`。
* **源虚拟机确认**：本任务以第 6 课已创建并安装好的 Ubuntu Server 虚拟机 `svr01` 为克隆源母本。可先执行 `virsh list --all` 确认列表中存在 `svr01`。
* **磁盘容量**：完整克隆会复制整块虚拟磁盘的物理占用（约 2~4GB），开始前请确保宿主机存储池目录 `/var/lib/libvirt/images` 拥有至少 20GB 以上的剩余空间。
  :::

### 【动手做】检查状态并执行完整克隆

在宿主机终端中依次执行以下步骤，完成从源机 `svr01` 到候选模板 `ubuntu-lab-template` 的克隆与磁盘链验证：

```bash
# 1. 检查宿主机存储空间与源虚拟机 svr01 运行状态
df -h /var/lib/libvirt/images
virsh domstate svr01

# 2. 若 svr01 处于 running 状态，必须先发送优雅关机指令
virsh shutdown svr01

# 注意：关机需要几秒钟时间，请重复执行下面这条命令，直到状态确认为 shut off
virsh domstate svr01

# 3. 状态变为 shut off 后，执行 virt-clone 完整克隆，创建候选模板虚拟机
virt-clone \
  --original svr01 \
  --name ubuntu-lab-template \
  --auto-clone

# 4. 验证新模板的虚拟磁盘路径与 backing-chain 独立性
virsh domblklist ubuntu-lab-template --details
qemu-img info --backing-chain /var/lib/libvirt/images/ubuntu-lab-template.qcow2
```

::: tip 参数拆解说明

* `--original svr01`：指定作为克隆母本的源虚拟机名称。
* `--name ubuntu-lab-template`：指定新克隆出的虚拟机 Domain 名称。
* `--auto-clone`：指示 libvirt 自动生成全新的虚拟磁盘文件名（默认存放在同一存储池中，命名为 `ubuntu-lab-template.qcow2`）。
  :::

### 【看现象】输出观察

执行上述命令后，观察终端中的关键回显：

1. **`virt-clone` 执行过程输出**：
   ```text
   Allocating 'ubuntu-lab-template.qcow2'       |  20 GB  00:00:12
   Clone 'ubuntu-lab-template' created successfully.
   ```
   *观察要点：`virt-clone` 自动在存储池中分配了全新的 `ubuntu-lab-template.qcow2` 镜像文件，并在十几秒内完成了全量块数据的物理复制。*

2. **`qemu-img info --backing-chain` 输出**：
   ```text
   image: /var/lib/libvirt/images/ubuntu-lab-template.qcow2
   file format: qcow2
   virtual size: 20 GiB (21474836480 bytes)
   disk size: 2.85 GiB
   cluster_size: 65536
   Format specific information:
       compat: 1.1
       compression type: zstd
   ```
   *观察要点：输出中**没有任何 `backing file` 行**，证明该 QCOW2 磁盘是一块 100% 独立的磁盘，与 `svr01.qcow2` 彻底解耦，即使后续修改或删除了 `svr01`，该模板镜像也不会受到任何影响。*

::: tip 通俗解析：什么是 backing file（母盘差量依赖）？
在 QCOW2 虚拟磁盘格式中，`backing file` 就像是在“原版教科书（母盘）”上盖了一张“透明硫酸纸（差量盘）”。你在差量盘上做的所有写操作都记在硫酸纸上，但读取内容时需要透过硫酸纸看底下的原版书。虽然这种“链接克隆”极度节省磁盘空间，但只要底下的原版书被不小心撕毁或涂改，所有硫酸纸上的笔记就会彻底失效报废。

而 `virt-clone` 执行的完整克隆，则是通过复印机**整本复印出一本完全相同的新书**（独立副本）。因此在 `qemu-img info` 中**不会出现任何 `backing file` 行**，代表两本教材各自独立、互不相欠，安全性与容错性最高。
:::

### 【学原理与拓展】交付机制、COW 写时复制与克隆底层数据流

#### 1. 虚拟机三大交付方式多维对比

在虚拟化与云计算体系中，从零部署虚拟机的常见方式对比如下：

| 交付方式 | 操作流程与数据流 | 交付耗时 | 优势 | 核心缺陷与风险 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **全新 ISO 安装** | 挂载光盘镜像 ➔ 交互式选择语言、分区、账号 ➔ 逐包解压写入 | 15 ~ 30 分钟 | 身份天然干净独立，无历史包袱 | 速度慢、重复算力消耗大、不同时期安装易导致软件版本配置漂移 | 制作最初始的基准种子系统 |
| **直接完整克隆** | 关机 ➔ `virt-clone` 物理复制 QCOW2 与 XML 配置 | 10 ~ 30 秒 | 秒级交付，软件包与环境 100% 相同 | **来宾内部身份原样复制**，直接运行会导致严重的局域网与集群冲突 | 单机离线备份或故障副本暂存 |
| **黄金模板派生** | 标准系统 ➔ **去泛化清理（cloud-init clean）** ➔ 固化模板 ➔ 克隆派生 | 10 ~ 30 秒 | 既拥有秒级交付速度，又能在开机时自动生成全新独立身份 | 需要严格遵守模板只读与维护规范 | 企业私有云、生产集群批量部署 |

#### 2. 完整克隆（Full Clone）vs 链接克隆（Linked Clone）底层数据流与 COW 机制

在 QEMU/KVM 底层，虚拟磁盘的克隆主要分为两类截然不同的实现方式：

```mermaid
flowchart TB
    subgraph FullClone ["完整克隆 (Full Clone) - 本课采用"]
        direction TB
        DiskA["svr01.qcow2\n(独立物理数据 2.8GB)"]
        DiskB["ubuntu-lab-template.qcow2\n(独立物理副本 2.8GB)"]
        DiskA -.->|"逐块物理复制 (Block Copy)\n完全解耦"| DiskB
    end

    subgraph LinkedClone ["链接克隆 (Linked Clone) - 写时复制 COW"]
        direction TB
        BaseDisk["Base-Image.qcow2\n(只读母盘 / Backing File)"]
        Overlay1["lab01-diff.qcow2\n(增量差量盘 100MB)"] -->|"backing_file 指向"| BaseDisk
        Overlay2["lab02-diff.qcow2\n(增量差量盘 150MB)"] -->|"backing_file 指向"| BaseDisk
    end
```

* **完整克隆（Full Clone）的数据流与底层结构**：
  * `virt-clone` 在执行时，调用底层的块设备复制接口，将源镜像文件中的所有已分配簇（Clusters）进行逐块物理复制（Block-by-Block Copy）；
  * 新生成的 QCOW2 文件拥有完全独立的 Header、L1/L2 查找表以及独立分配的数据簇；
  * **独立性**：克隆完成后，克隆机与母机在物理存储上彻底切断联系，源盘的损坏或删除绝不会影响新虚拟机。
* **链接克隆（Linked Clone）的 COW（Copy-on-Write，写时复制）机制**：
  * 链接克隆通过 `qemu-img create -f qcow2 -F qcow2 -b Base.qcow2 Overlay.qcow2` 创建；
  * **读取流程**：当虚拟机尝试读取某个扇区时，QEMU 首先查询 Overlay 盘的 L2 表。如果该扇区在 Overlay 中已写入（已修改），则直接从 Overlay 读取；如果尚未被修改，则沿着 `backing_file` 指针回溯到 Base 基础镜像中读取原始数据；
  * **写入流程（COW）**：当虚拟机首次向某个扇区写入新数据时，QEMU 不会修改 Base 镜像（Base 保持只读），而是在 Overlay 盘中临时分配一个新的 Cluster（通常为 64KB），将修改后的数据写入 Overlay，并在 Overlay 的 L2 表中建立新映射。后续针对该位置的读写都直接在 Overlay 上进行。

下表总结了两种克隆模式的多维度对比：

| 对比维度 | 完整克隆（Full Clone） | 链接克隆（Linked Clone） |
| :--- | :--- | :--- |
| **物理存储占用** | 每次克隆占用源机全部实际数据量（约 2~4GB/台） | 极小，初期每台差量盘仅几百 KB ~ 几十 MB |
| **创建耗时** | 取决于磁盘大小与 I/O 速度，通常 10 ~ 30 秒 | 瞬间完成（毫秒级创建 Overlay 文件） |
| **母盘容错隔离性** | **100% 独立解耦**，母盘删除或修改无任何影响 | **强依赖**，母盘一旦损坏或被误修改，所有子机全部崩溃 |
| **I/O 链条深度** | 单层直读直写，性能稳定损耗小 | 读操作可能经历多层回溯，随差量膨胀存在轻微 I/O 开销 |
| **生产适用场景** | 生产服务器、长期运行的独立业务 VM、模板固化 | 自动化 CI/CD 测试流水线、学生短期实验沙箱、VDI 桌面云 |

#### 3. 关键技术辨析：禁止直接使用 Linux `cp` 命令拷贝虚拟磁盘的底层原理

很多初学者可能会产生疑问：“虚拟磁盘在 Linux 宿主机上就是一个 `.qcow2` 文件，为什么不能直接在宿主机执行 `cp svr01.qcow2 clone.qcow2` 呢？”

在虚拟化架构中，直接使用 `cp` 拷贝存在三大严重缺陷与风险：

1. **QEMU 文件锁与元数据破坏风险**：
   当虚拟机处于运行状态时，QEMU 进程会对 QCOW2 文件持有排他文件锁（File Lock），并且内存中有大量尚未落盘的脏页（Dirty Pages）。此时直接用 `cp` 拷贝出来的文件处于元数据不一致状态，极易导致克隆出来的镜像在开机引导时出现文件系统损坏（Kernel Panic 或 Emergency Mode）。
2. **稀疏文件（Sparse File）与空洞（Hole）膨胀问题**：
   QCOW2 和 RAW 磁盘通常包含大量未分配的空洞（Holes）。普通的 `cp` 命令如果没有带特殊参数（如 `--sparse=always`），在读取空洞时会将其填充为实际的二进制零并写入磁盘，导致原本仅占用 2.8GB 的精简置备镜像在复制后物理空间瞬间膨胀为完整的 20GB，大量挤占宿主机存储。
3. **libvirt Domain XML 身份缺失与冲突**：
   虚拟机的完整定义由“虚拟磁盘文件”与“Domain XML 配置文件”共同构成。直接 `cp` 磁盘文件并没有在 libvirt 中注册新的虚拟机对象；如果管理员手动复制 XML 文件并导入，又会导致虚拟机名称（Name）、硬件 UUID、网卡 MAC 地址与原虚拟机完全重名与冲突。
   而 `virt-clone` 不仅安全地复制磁盘数据，还会自动为新虚拟机生成独立的 Domain XML、分配全新的 UUID 与 MAC 地址，并在 libvirt 体系中完成注册。

#### 4. 关机门禁铁律与数据一致性保护

正如前面所述，QEMU 在运行期间维护着活跃的文件系统事务。因此，**在执行克隆前必须确认源虚拟机处于 `shut off` 状态**，这是保证克隆镜像数据完整性与文件系统一致性的安全铁律。

## 7.2 任务二：探寻身份冲突——宿主侧与来宾侧多重身份解密

通过 `virt-clone` 成功克隆出虚拟机后，很多初学者会误以为“两台机器已经完全独立，可以直接并网运行了”。为了看清隐藏在系统内部的隐患，我们需要同时启动两台虚拟机进行对比实验。

::: tip 实用操作技巧：如何登录来宾控制台并返回宿主机？
在宿主机终端执行 `virsh console <虚拟机名>` 即可连接来宾系统的文本控制台：

* **激活提示符**：连接后若屏幕暂时静止无回显，**敲击一次回车键**即可唤出 `login:` 登录提示符；
* **用户登录**：输入在第 6 课安装系统时设置的管理员用户名与密码；
* **安全退出**：完成内部命令操作后，按下快捷键 Ctrl + ]（macOS 用户按 Control + ]），即可立即脱离虚拟机控制台，安全返回宿主机终端。
  :::

### 【动手做】同时开机并对比多重身份

在宿主机终端与虚拟机内部执行以下操作，复现身份冲突现象：

```bash
# 1. 在宿主机终端同时启动两台虚拟机
virsh start svr01
virsh start ubuntu-lab-template

# 2. 在宿主机终端比对两台虚拟机的宿主侧身份（Domain、UUID、MAC、磁盘）
for vm in svr01 ubuntu-lab-template; do
  echo "==================== Domain: $vm ===================="
  virsh dominfo $vm | grep -E "Name|UUID|State"
  virsh domiflist $vm
  virsh domblklist $vm --details
done
```

分别通过 `virsh console`（或 SSH）登录到 `svr01` 与 `ubuntu-lab-template` 虚拟机内部，执行以下内部身份查询命令：

```bash
# 在虚拟机内部执行（分别登录 svr01 与 ubuntu-lab-template 运行）
echo "Hostname: $(hostnamectl --static)"
echo "Machine ID: $(cat /etc/machine-id)"
ip -4 -br addr show scope global
```

观察完毕后，按下 Ctrl + ] 返回宿主机终端，安全关闭源机 `svr01`，保持其作为基准不被改动：

```bash
# 在宿主机终端安全关闭 svr01
virsh shutdown svr01
```

### 【看现象】输出观察与对比

两台虚拟机运行后的宿主侧与来宾侧各项关键身份对比如下表所示：

| 身份指标 | 所在层级 | 源机 `svr01` | 克隆机 `ubuntu-lab-template` | 对比结果 |
| :--- | :--- | :--- | :--- | :--- |
| **Domain 名称** | 宿主 Hypervisor | `svr01` | `ubuntu-lab-template` | **完全独立**（libvirt 虚拟机外壳唯一标识） |
| **libvirt UUID** | 宿主硬件层 | `a1b2c3d4-e5f6-...` | `f8e7d6c5-b4a3-...` | **完全独立**（硬件 UUID 重新随机生成） |
| **磁盘镜像路径** | 宿主存储层 | `/var/lib/.../svr01.qcow2` | `/var/lib/.../ubuntu-lab-template.qcow2` | **完全独立**（物理上完全解耦的独立文件） |
| **虚拟网卡 MAC** | 宿主网络层 | `52:54:00:12:34:56` | `52:54:00:ab:cd:ef` | **完全独立**（二层物理地址重新随机生成） |
| **主机名 Hostname** | 来宾系统内部 | `svr01` | `svr01` | 严重重复！（完全继承源机） |
| **systemd machine-id**| 来宾系统内部 | `e4d3c2b1...` | `e4d3c2b1...` | 严重冲突！（32位全局唯一标识完全相同） |
| **IP / Netplan 配置** | 来宾网络协议栈 | `192.168.122.101` | `192.168.122.188` (DHCP) | 依赖 DHCP 时暂不相同，若为静态 IP 则冲突 |

### 【学原理与拓展】身份体系拆解与三大分布式冲突灾难推导

#### 1. 宿主侧 vs 来宾内部的 Hypervisor 隔离屏障

`virt-clone` 是一个纯粹运行在宿主机用户态的管理工具。它通过调用 libvirt API 复制 XML 配置文件与磁盘镜像文件，并为宿主侧所关心的 Domain 名称、硬件 UUID、虚拟网卡 MAC 地址自动生成新的随机值。

但是，**Hypervisor 与虚拟磁盘内部的文件系统之间存在天然的隔离屏障**。`virt-clone` 无法也不会被允许去挂载虚拟机的根文件系统并擅自修改 `/etc/hostname` 或 `/etc/machine-id`。

```mermaid
flowchart TD
    subgraph HostSide ["宿主机 Hypervisor 层面 (virt-clone 自动处理)"]
        DName["Domain 名称: 自动重命名 ✓"]
        DUUID["Domain UUID: 自动重新生成 ✓"]
        DMAC["网卡 MAC 地址: 自动随机分配 ✓"]
        DDisk["虚拟磁盘文件: 自动分配独立副本 ✓"]
    end

    subgraph GuestSide ["虚拟机 Guest 内部层面 (virt-clone 无法穿透修改)"]
        GHost["主机名 (hostname): 残留旧值 svr01 ✕"]
        GMID["/etc/machine-id: 残留旧哈希 ✕"]
        GIP["网络 IP / Netplan: 残留旧网络缓存 ✕"]
    end

    VirtClone["virt-clone 命令"] --> HostSide
    VirtClone -.->|"受限于隔离屏障\n无法修改内部文件"| GuestSide
```

#### 2. `/etc/machine-id` 的核心作用与三大企业级冲突灾难

`/etc/machine-id` 是现代 Linux（systemd）操作系统中至关重要的 **32 位十六进制全局唯一标识符**（由 128 位随机数生成）。它是整个操作系统实例在分布式网络中的“数字身份证”。一旦多台克隆机器带着相同的 `machine-id` 在同一网络中运行，将引发以下三大破坏性灾难：

```mermaid
flowchart TB
    SameID["未经清理克隆\n残留相同 /etc/machine-id"] --> Disaster1["灾难一：Kubernetes 集群\n节点覆盖与 Pod 震荡"]
    SameID --> Disaster2["灾难二：systemd-journald\n集中式日志交叉污染与覆盖"]
    SameID --> Disaster3["灾难三：DHCP DUID 抢占\n局域网 IP 踩踏与断连震荡"]

    Disaster1 --> D1_Desc["Kubelet 读取相同 Node UID\nMaster 误判同节点覆盖心跳\n两节点互踢，Pod 疯狂漂移"]
    Disaster2 --> D2_Desc["日志主键同名\nLoki / ELK 混合入库\n排错链路彻底断裂"]
    Disaster3 --> D3_Desc["DUID-UUID 相同\nDHCP 误判同一设备重分配\n两机争抢同一 IP 周期断网"]
```

##### 灾难一：Kubernetes 集群节点注册覆盖与 Pod 震荡灾难

* **触发机制**：在 Kubernetes 集群中，工作节点上的 `kubelet` 守护进程启动时，会读取 `/etc/machine-id`（结合 DMI UUID）作为该 Node 对象的唯一物理标识（Node UID）。
* **破坏后果**：当运维人员利用未清理的模板克隆出 Node2 并加入集群时，由于 Node2 与既有的 Node1 具有相同的 `machine-id`，Kubernetes API Server 会将 Node2 判定为“Node1 重新上线并上报了新的状态”。API Server 会将 Node1 的状态覆盖为 Node2 的 IP，随后发现 Node1 原有的心跳中断，判定 Node1 为 `NotReady`，并在整个集群中发起 Pod 驱逐；紧接着 Node1 的 kubelet 再次上报心跳，又将 Node2 覆盖掉。两台物理节点在控制平面互相踢掉对方，导致集群调度器混乱，业务 Pod 在两台机器之间疯狂漂移和重启。

##### 灾难二：systemd-journald 集中式日志追踪失效与交叉污染

* **触发机制**：`systemd-journald` 服务将本地二进制系统日志写入 `/var/log/journal/<machine-id>/` 目录中。而在现代企业级可观测性体系（如 Promtail -> Loki、Filebeat -> Elasticsearch、Vector）中，日志采集 Agent 通常直接以 `/etc/machine-id` 作为主机的唯一索引标识。
* **破坏后果**：由于多台虚拟机的 `machine-id` 完全相同，日志收集服务端会将来自不同业务节点（例如一台跑数据库、一台跑 Web）的日志混合写入同一个时间流中。由于时钟微秒级差异，日志时间戳严重错乱，数据库的错误日志与 Web 服务的访问日志混杂在一起，导致监控告警误报、根本原因定位彻底失效。

##### 灾难三：DHCP DUID 抢占与网络断连震荡灾难

* **触发机制**：在现代 Linux（使用 `systemd-networkd` 或 NetworkManager 的 Ubuntu 系统）中，DHCP 客户端向网络中的 DHCP 服务器申请 IP 地址时，默认不再仅仅使用网卡 MAC 地址，而是生成 RFC 3315 / RFC 8415 规定的 **DUID（DHCP Unique Identifier）**。默认的 `DUID-UUID` 算法直接强依赖 `/etc/machine-id`。
* **破坏后果**：虽然两台虚拟机的虚拟网卡 MAC 地址在宿主机层面不同，但向 DHCP 服务器发送 DHCPREQUEST 时出示的 DUID 却完全相同。DHCP 服务器判定“这是同一台设备更换了网卡接口”，于是将原本分配给 A 机的 IP 强行收回并改发给 B 机；A 机检测到 IP 冲突或租约异常后再次发起请求，又把 IP 抢回。两台虚拟机在局域网内疯狂争抢同一个 IP 地址，引发周期性网络丢包与断连震荡。

::: tip 通俗解析：三大灾难的生动生活比喻
为了彻底理解为什么底层 `machine-id` 绝不能重复，我们可以用三个生活化场景来类比：

1. **Kubernetes 节点覆盖（共用身份证打卡）**：两个人拿着**同一张身份证**去公司考勤机打卡，A 刚刷完进门，B 接着刷同一张卡，系统误以为“A 换了个工位”，主管以为 A 旷工把 A 的办公桌清空搬走；A 再次打卡又把 B 顶替，两人在办公室疯狂抢工位！
2. **日志交叉污染（混装日记本）**：财务小张与厨师老李各自写工作日记，但封面都印着**同一个身份证号**，档案管理员把两本日记撕开混装在同一个档案袋里。审计员翻开一看，上一页是“收到货款 10 万元”，下一页紧接着“买白菜花费 20 元”，账目彻底陷入逻辑崩溃！
3. **DHCP DUID 抢占（临时工牌与身份证）**：MAC 地址好比员工佩戴的**临时出入证**，而 `/etc/machine-id` 生成的 DUID 好比**法定身份证**。如果两台克隆机戴着不同的临时工牌（MAC 不同），但向门卫登记时出示的是同一张身份证，门卫就会误以为“这是同一个人换了件外套又来办业务”，把同一把办公室钥匙在两人手中来回抢夺，导致两人轮流被关在门外断网！
   :::

为了直观体验多重身份在未清理与修复状态下的对比，请操作以下交互式动画：

## 7.3 任务三：制作黄金模板——cloud-init 状态清理与系统去重

为了彻底根除克隆带来的内部身份冲突，虚拟化运维的标准流程是制作**黄金模板（Golden Template）**。我们需要在模板虚拟机内部执行“去泛化（Generalization）”操作，清除所有旧实例的痕迹。

### 【动手做】执行 cloud-init 清理与黄金模板固化

通过 `virsh console ubuntu-lab-template` 登录到 `ubuntu-lab-template` **虚拟机内部**，执行主机名重置、cloud-init 清理与安全关机：

```bash
# 【以下命令均在 ubuntu-lab-template 虚拟机内部执行】

# 1. 将主机名设置为通用的模板维护标识
sudo hostnamectl set-hostname ubuntu-lab-template

# 2. 执行 cloud-init clean 清理实例缓存并重置 machine-id 为未初始化状态
sudo cloud-init clean --machine-id

# 3. 立即执行安全关机，完成模板固化（严禁在此刻重启！）
sudo systemctl poweroff
```

::: warning 关键避坑指南：清理后严禁重启，必须立即 poweroff 关机！
在虚拟机内部执行 `sudo cloud-init clean --machine-id` 之后，**千万不要执行 reboot 重启**！
如果执行了重启，系统在引导启动阶段会自动探测到 machine-id 为空，并立即生成一个新的 machine-id 和 cloud-init 缓存，刚才所做的去泛化清理将瞬间前功尽弃！
因此，清理完成后必须紧接着执行 `sudo systemctl poweroff` 立即关机固化，让模板保持在纯净的“出厂封存状态”。
:::

虚拟机完全断电后，控制台会自动断开返回宿主机。在宿主机终端验证模板是否已经处于安全的关机状态：

```bash
# 【在宿主机终端执行】
# 4. 验证 ubuntu-lab-template 已完全关机
virsh domstate ubuntu-lab-template
```

### 【看现象】输出观察

1. **`sudo cloud-init clean --machine-id` 执行现象**：
   命令执行后，终端静默返回或打印清理日志。此时若查看 `/etc/machine-id` 文件：
   ```bash
   ls -l /etc/machine-id
   cat /etc/machine-id
   ```
   *观察要点：`/etc/machine-id` 文件大小变为 0 字节（或内容被替换为 `uninitialized` 标志）。同时，`/var/lib/cloud/instances/` 下的历史实例目录被全部清除。*

2. **宿主机端状态确认**：
   `virsh domstate ubuntu-lab-template` 明确返回 `shut off`。

### 【学原理与拓展】cloud-init 四阶段流水线与黄金模板五项运维铁律

#### 1. cloud-init 四阶段执行流水线架构（4-Stage Pipeline）深度剖析

Ubuntu Server 默认深度集成了 `cloud-init` 作为其云原生初始化引擎。理解 cloud-init 的执行生命周期，是掌握现代操作系统自动化配置的关键。cloud-init 在系统开机时被划分为清晰的四个执行阶段：

```mermaid
flowchart TD
    subgraph S1 ["Stage 1: Generator 阶段 (cloud-init-local.service)"]
        S1_1["本地文件系统挂载后立即执行"]
        S1_2["扫描本地数据源 (NoCloud / ConfigDrive)"]
        S1_3["阻断网络前写入基础 Netplan 配置"]
    end

    subgraph S2 ["Stage 2: Network 阶段 (cloud-init.service)"]
        S2_1["网络子系统上线后执行 (network.target)"]
        S2_2["连接元数据服务获取 Metadata / User-data"]
        S2_3["写入系统主机名并同步 /etc/hosts"]
    end

    subgraph S3 ["Stage 3: Config 阶段 (cloud-config.service)"]
        S3_1["基础系统就绪后执行"]
        S3_2["磁盘自动扩容 (growpart / resize2fs)"]
        S3_3["注入 SSH 密钥与创建指定系统用户"]
    end

    subgraph S4 ["Stage 4: Final 阶段 (cloud-final.service)"]
        S4_1["系统引导末端 (multi-user.target 之后)"]
        S4_2["安装指定软件包 packages"]
        S4_3["执行 runcmd / bootcmd 自定义脚本"]
        S4_4["写入 boot-finished 标记完成通知"]
    end

    S1 --> S2 --> S3 --> S4
```

* **Stage 1: Local 阶段（`cloud-init-local.service`）**：
  * **运行条件**：在根文件系统挂载后、网络启动前极早阶段执行。
  * **职责**：扫描本地即插即用数据源（如 NoCloud、ConfigDrive、内核 cmdline）。如果找到了网络配置定义，在此阶段直接生成 `/etc/netplan/` 配置文件，使得后续网络服务能直接读取并拉起网络。
* **Stage 2: Network 阶段（`cloud-init.service`）**：
  * **运行条件**：在网络服务（`network.target`）完全就绪后执行。
  * **职责**：通过网络请求远程元数据服务（如 AWS IMDS、OpenStack Metadata）或解析本地完整配置；根据配置设定系统的主机名（Hostname），并自动将其与 `127.0.1.1` 的映射写入 `/etc/hosts`。
* **Stage 3: Config 阶段（`cloud-config.service`）**：
  * **运行条件**：在绝大多数核心系统服务启动期间运行。
  * **职责**：执行各种具体的配置模块（`cc_*` 模块），例如调用 `growpart` 自动扩展根分区并调整文件系统大小、在目标用户目录下写入 `~/.ssh/authorized_keys` 注入管理员公钥、配置 APT 国内镜像软件源等。
* **Stage 4: Final 阶段（`cloud-final.service`）**：
  * **运行条件**：在操作系统几乎全部启动完成、进入 `multi-user.target` 的最后时刻执行。
  * **职责**：执行耗时较长的定制任务，例如自动下载并安装软件包列表（`packages` 字段）、执行管理员编写的 `runcmd` / `bootcmd` Shell 脚本，最后在 `/var/lib/cloud/instance/boot-finished` 写入标记文件，向外部发送启动完成信号。

::: tip 通俗解析：cloud-init 四阶段流水线就像“新房精装交付四步走”
为了轻松记住这四个阶段的先后顺序与底层逻辑，可以将其类比为房屋交付流程：

1. **Stage 1: Local 本地阶段（毛坯水电管线预埋）**：此时系统还没通外网，先根据本地施工图纸（本地数据源）把基础管线（Netplan 网络配置文件）在通网前铺好；
2. **Stage 2: Network 网络阶段（接通外网与钉门牌号）**：外部网络接通，联网获取业主数据，正式挂上专属门牌号（Hostname），并登记在小区名录（`/etc/hosts`）上；
3. **Stage 3: Config 配置阶段（搬入家具与配大门钥匙）**：基础系统就绪，自动把房间按需扩建（磁盘 `growpart` 自动扩容）、配发业主大门钥匙（注入 SSH 公钥）、创建家庭成员账号（创建用户）；
4. **Stage 4: Final 交付阶段（全屋保洁与交付剪彩）**：系统即将完全就绪，安装指定应用软件（packages 列表）、执行业主特别吩咐的入住脚本（runcmd），最后在门口贴上“验收合格竣工证”（写入 `boot-finished` 标记）。
   :::

#### 2. `cloud-init clean --machine-id` 底层清除清单剖析

当我们执行 `sudo cloud-init clean --machine-id` 时，系统底层完成了以下高精度的清理操作：

1. **截断 machine-id**：清空 `/etc/machine-id` 与 `/var/lib/dbus/machine-id`，将其重置为 0 字节或 `uninitialized` 标志；
2. **清除实例元数据缓存**：递归删除 `/var/lib/cloud/data/`、`/var/lib/cloud/instance/` 与 `/var/lib/cloud/instances/*`，使 cloud-init 彻底忘记曾经运行过的实例身份；
3. **重置执行信号量**：清空 `/var/lib/cloud/sem/` 状态锁，确保下一次开机时，cloud-init 的全部四个阶段与所有配置模块能够重新按全新实例被触发；
4. **清除网络临时租约**：清除 DHCP 客户端的临时租约记录，确保下次开机向网络发起全新的初始广播。

::: tip 通俗解析：什么是“去泛化（Generalization）”？
“去泛化”就像智能手机出厂前的“恢复出厂设置并封盒入库”：

1. 抹掉上一任测试员的所有操作痕迹和实例缓存；
2. 将手机序列号标记为“未激活”；
3. 封盒关机（固化为 Golden Template）。

当这台手机被下一位使用者首次通电开机（First Boot）时，系统才会像新手机激活向导一样，自动为它生成全新的专属身份！
:::

#### 3. 黄金模板（Golden Template）五项运维铁律

制作完成的黄金模板是整个机房与集群的“母盘”，所有运维人员必须严格恪守以下五项运维铁律：

::: danger 黄金模板五项运维铁律

1. **无残留静态配置**：网络配置必须保留为干净的 DHCP 动态模式，严禁硬编码静态 IP、固定网关或私有 DNS；
2. **无个人密钥与认证痕迹**：清空 `/root/.ssh/` 与普通用户的 `authorized_keys`，清空 `~/.bash_history` 与临时认证 Token；
3. **无历史日志与系统缓存**：清空 `/var/log` 下的历史归档日志，执行 `sudo apt-get clean` 清除软件包缓存；
4. **状态去泛化固化**：在关机前必须标准执行 `sudo cloud-init clean --machine-id`，将系统置于去泛化的就绪状态；
5. **常年保持 shut off 关机**：黄金模板只用于作为 `virt-clone` 的克隆源，平时必须常年保持关机状态，严禁直接开机运行任何业务。
   :::

我们可以通过以下流水线动画，完整回顾从源机制作黄金模板再到批量派生的全生命周期：

## 7.4 任务四：从模板极速派生——批量交付 lab01 与 NoCloud 企业级自动化

黄金模板固化后，我们就可以利用它在十几秒内极速派生出全新的、且内部身份完全独立的业务虚拟机 `lab01`。

### 交付规格参数表

| 虚拟机名称 | 来源母本 | vCPU / 内存 | 虚拟磁盘镜像路径 | 预期 Hostname | 预期 Machine-ID | 网络模式 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **`lab01`** | `ubuntu-lab-template` | 2 核 / 2048 MiB | `/var/lib/libvirt/images/lab01.qcow2` | `lab01` | 开机自动生成全新 32 位 ID | default (NAT) 独立 DHCP |

### 【动手做】派生 lab01 并验收身份独立性

在宿主机终端中执行派生命令并启动 `lab01`：

```bash
# 1. 在宿主机终端从黄金模板派生出 lab01
virt-clone \
  --original ubuntu-lab-template \
  --name lab01 \
  --auto-clone

# 2. 启动全新派生的 lab01 虚拟机
virsh start lab01
```

登录到 `lab01` 虚拟机内部（在宿主机执行 `virsh console lab01`，按回车激活并输入登录账号密码），执行首次启动身份配置与验证：

::: tip 为什么需要为派生机配置专属主机名？
从黄金模板克隆出的新虚拟机 `lab01` 首次开机时，`systemd` 会自动为其生成全新的 `machine-id`，但其内部主机名仍会继承模板名 `ubuntu-lab-template`。为了在多节点网络环境中准确区分业务节点，我们在首次登录后执行 `sudo hostnamectl set-hostname lab01` 将其修改为专属主机名。
:::

```bash
# 【以下命令在 lab01 虚拟机内部执行】

# 3. 验证 machine-id 是否已由 systemd 自动新生
cat /etc/machine-id

# 4. 设置 lab01 专属业务主机名
sudo hostnamectl set-hostname lab01

# 5. 验证网络接口与 DHCP 获取到的独立 IP 地址
ip -4 -br addr show scope global
```

查看完毕后，按下 Ctrl + ] 返回宿主机终端，运行全量巡检脚本，比对三台虚拟机的各项身份指标：

```bash
# 【在宿主机终端执行】
# 6. 执行跨机器全量身份比对
for vm in svr01 ubuntu-lab-template lab01; do
  echo "==================== Domain: $vm ===================="
  virsh domstate $vm
  virsh domuuid $vm
  virsh domiflist $vm
  virsh domblklist $vm --details
done
```

### 【看现象】输出观察与验收结论

1. **`lab01` 内部 machine-id 验证**：
   在 `lab01` 内执行 `cat /etc/machine-id`，输出为一个与 `svr01` 截然不同的全新哈希值（例如 `9f8e7d6c5b4a3210fedcba9876543210`），证明 `cloud-init` 与 `systemd` 的首次启动重生机制完美生效！
2. **全量身份独立性验收**：
   * `svr01`、`ubuntu-lab-template` 与 `lab01` 的 Domain 名称、UUID、磁盘路径、MAC 地址 100% 互不相同；
   * `lab01` 的主机名为 `lab01`，与 `svr01` 互不干扰；
   * `lab01` 的 `machine-id` 唯一且独立；
   * `lab01` 通过 DHCP 成功获取到了专属的局域网 IP 地址，网络通信正常。

### 【学原理与拓展】First Boot 重生机制与 NoCloud 企业级全自动交付

#### 1. First Boot（首次启动）去泛化激活机制闭环

当以黄金模板为基础派生出的 `lab01` 第一次通电引导时，系统在启动早期阶段依次完成以下自动化闭环：

1. **内核与 systemd 初始化**：Linux 内核完成硬件自检，启动 `systemd`（PID 1）；
2. **熵池采集与 ID 新生**：`systemd-machine-id-setup` 探测到 `/etc/machine-id` 为空（或标记为 uninitialized），立即从内核熵池（KVM VirtIO-RNG 随机数发生器）读取 128 位硬件熵，计算生成全新的 32 位十六进制 UUID 并固化写入 `/etc/machine-id`；
3. **网络与协议栈就绪**：`systemd-networkd` 与 DHCP 客户端基于全新的 `machine-id` 动态计算生成专属的 DUID，向网关申请独立的 IP 租约；
4. **cloud-init 激活初始化**：cloud-init 检测到新的实例环境，重新执行全生命周期配置。

#### 2. 企业级拓展：NoCloud 数据源与 cidata.iso 无人值守自动化注入

在大型公有云或私有云平台中，运维工程师通常不需要手动登录每台虚拟机去执行 `hostnamectl` 或配置用户。cloud-init 支持一种非常强大的轻量级本地数据源——**NoCloud 数据源**。

通过在宿主机制作一张包含了 `meta-data` 和 `user-data` 的极小 ISO 光盘镜像（卷标必须为 `cidata`），并挂载给新虚拟机，虚拟机开机时 cloud-init 的 Local 阶段就会自动读取该光盘，在 **5 秒内全自动完成主机名、网络、用户与 SSH 密钥的注入**。

::: tip 通俗解析：什么是 NoCloud cidata.iso？
它就像给新电脑开箱时附赠的\*\*“自运行配网 U 盘”\*\*：
只要把包含配置说明书（`meta-data` 实例元数据和 `user-data` 初始化配置）的极小虚拟光盘插入虚拟机光驱，虚拟机在首次开机通电的 5 秒内，就会全自动把主机名、网络、用户账号与 SSH 密钥全部配置就绪，运维工程师甚至不需要敲击一行键盘！
:::

##### （1）meta-data 配置文件语法

`meta-data` 主要用于描述实例的基础元数据：

```yaml
# meta-data 文件内容示例
instance-id: lab01-instance-001
local-hostname: lab01
```

##### （2）user-data 配置文件语法（cloud-config）

`user-data` 遵循 `#cloud-config` 规范，用于定义用户、认证与自定义执行脚本：

```yaml
#cloud-config
# user-data 文件内容示例
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... admin@company.com

# 自动扩展根文件系统
growpart:
  mode: auto
  devices: ['/']

# 自定义开机执行命令
runcmd:
  - echo "Instance lab01 initialized successfully at $(date)" > /etc/motd
```

##### （3）生成 cidata.iso 并挂载启动的完整自动化流水线

在宿主机中，可通过 `genisoimage` 或 `cloud-localds` 工具生成 ISO 并挂载：

```bash
# 在宿主机制作 cidata.iso 并挂载给虚拟机的操作流程示例（供拓展阅读）
# 1. 创建包含配置的 ISO 镜像（卷标必须为 cidata）
genisoimage -output cidata-lab01.iso -volid cidata -joliet -rock meta-data user-data

# 2. 将 cidata.iso 作为虚拟光驱挂载并启动虚拟机
virt-install \
  --name lab01 \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/lab01.qcow2,bus=virtio \
  --disk path=/var/lib/libvirt/images/cidata-lab01.iso,device=cdrom \
  --osinfo detect=on,require=off \
  --network network=default,model=virtio \
  --import --noautoconsole
```

虚拟机首次启动时，cloud-init 会自动从 `cidata.iso` 读取配置，完成全自动初始化。

#### 3. 批量交付进阶：Shell 循环秒级派生集群

掌握了黄金模板克隆技术后，若需要为班级或测试团队交付 5 台服务器（`lab01` ~ `lab05`），只需在宿主机编写几行简单的 Shell 循环即可实现全自动化秒级交付：

```bash
# 批量极速派生 5 台独立实验机示例（在宿主机终端执行）
for i in $(seq -w 1 5); do
  echo "正在派生 lab$i..."
  virt-clone --original ubuntu-lab-template --name "lab$i" --auto-clone
  virsh start "lab$i"
done
```

## 7.5 常见问题排错矩阵 (Ubuntu 26.04 专属排错)

在 Ubuntu 26.04 LTS（libvirt 12.0+、QEMU 9.0+、cloud-init）环境下进行克隆与模板实训时，常见故障与排错方案如下：

| 故障现象 | 根因分析 | 检查定位命令 | 修复与对策 |
| :--- | :--- | :--- | :--- |
| `virt-clone` 报错 `ERROR Domain 'svr01' is still running` | 源虚拟机未关机，libvirt 为防止磁盘数据损坏触发安全拦截 | `virsh domstate svr01` | 在宿主机执行 `virsh shutdown svr01`，等待状态确认为 `shut off` 后再克隆 |
| `virt-clone` 报错 `ERROR Domain '...' already exists` | 宿主机已存在同名的虚拟机定义或残留 XML | `virsh dominfo <名称>` | 更换新名称，或使用 `virsh undefine <旧名称>` 移除废弃虚拟机定义 |
| 克隆过程中提示 `No space left on device` | 宿主机存储池物理磁盘空间不足 | `df -h /var/lib/libvirt/images` | 清理宿主机无用的大文件或快照，确保存储目录有至少 20GB 以上可用空间 |
| 连接 `virsh console` 控制台后屏幕卡住无任何反应 | 控制台尚未激活串行 TTY 通信 | 观察屏幕终端 | **敲击一次回车键**即可唤出 `login:` 登录提示符；若想退出按快捷键 Ctrl + ] |
| `lab01` 启动后 `machine-id` 依然与 `svr01` 一模一样 | 模板制作时遗漏了 `cloud-init clean --machine-id` 或清理后又开机运行了业务 | 在来宾内部执行 `cat /etc/machine-id` | 在 `ubuntu-lab-template` 内重新执行 `sudo cloud-init clean --machine-id` 并立即关机，重新克隆派生 |
| 两台虚拟机 MAC 地址不同但获取到了相同的 IP 地址 | 源机内部残留了写死的静态 IP 配置，或静态网络文件未改为 DHCP | 来宾内部执行 `ip -4 -br addr`，检查 `/etc/netplan/*.yaml` | 编辑 Netplan 配置文件，将网卡设置为 `dhcp4: true`，执行 `sudo netplan apply` |
| 登录 `lab01` 控制台时卡在 `cloud-init` 等待界面 | cloud-init 正在首次启动尝试连接数据源或检索网络元数据 | `cloud-init status --long` | 属于正常网络探测现象，稍等 10~15 秒即可自动跳过进入登录提示符 |
| `qemu-img info` 检查发现存在 `backing file` 行 | 误使用了 `qemu-img create -b` 差量方式而非 `virt-clone` 完整克隆 | `qemu-img info --backing-chain <磁盘路径>` | 若需要完全独立镜像，使用 `qemu-img convert -O qcow2 <差量盘> <新独立盘>` 进行合并固化 |

## 7.6 提交内容

完成本课实训后，请提交以下 **3 张核心终端验收截图**：

* **截图 1：virt-clone 完整克隆与独立磁盘验证**
  * 在宿主机执行 `virt-clone --original svr01 --name ubuntu-lab-template --auto-clone` 成功创建；
  * 紧接着执行 `qemu-img info --backing-chain /var/lib/libvirt/images/ubuntu-lab-template.qcow2`，截图中清晰展示 `file format: qcow2` 且**无 backing file 依赖**。
* **截图 2：身份冲突复现与黄金模板去重固化**
  * 同时启动两台机器，内部执行 `cat /etc/machine-id` 证实两台机器拥有完全相同的 32 位 ID（复现冲突）；
  * 随后在 `ubuntu-lab-template` 内部执行 `sudo cloud-init clean --machine-id` 并安全关机，宿主机执行 `virsh domstate ubuntu-lab-template` 显示 `shut off`。
* **截图 3：lab01 首次启动新生 ID 与三节点全量巡检**
  * 在 `lab01` 内部执行 `cat /etc/machine-id`（显示新生的唯一 ID）与 `hostnamectl`（显示 `lab01`）；
  * 在宿主机终端执行 `for` 循环巡检脚本，截图中清晰展示 `svr01`、`ubuntu-lab-template`、`lab01` 三台虚拟机的 Domain 名称、UUID、MAC 与磁盘路径完全独立。

## 本章小结

在本课中，我们从单台虚拟机的繁琐安装演进到了工业级的“黄金模板 + 极速克隆”自动化交付体系：

1. **掌握了 virt-clone 完整克隆与底层数据流**：理解了完整克隆（Full Clone）在物理存储上的独立性，对比了与链接克隆（Linked Clone）及 COW 写时复制机制的本质区别，剖析了不可直接使用 `cp` 拷贝磁盘的底层原因，严格恪守了源机必须处于 `shut off` 关机状态的安全门禁，学会使用 `qemu-img info --backing-chain` 验证磁盘独立性；
2. **看清了虚拟机多重身份体系与三大灾难**：深刻认识到 Hypervisor 隔离屏障的存在——宿主侧身份（Domain、UUID、磁盘、MAC）由 `virt-clone` 自动处理，而内部身份（hostname、`/etc/machine-id`、IP / Netplan）必须由运维人员主动介入去重；深度推导了 machine-id 冲突引发的 Kubernetes 节点互踢、journald 日志污染与 DHCP DUID 抢占三大分布式灾难；
3. **拆解了 cloud-init 四阶段流水线与黄金模板铁律**：深入理解了 cloud-init 从 Generator、Network、Config 到 Final 的 4-Stage 执行架构，掌握了 `sudo cloud-init clean --machine-id` 的底层清理细节，牢固树立了黄金模板五项运维铁律；
4. **达成了秒级实例批量交付与自动化拓展**：体验了从黄金模板到 `lab01` 的极速派生，见证了 systemd 在 First Boot 阶段自动新生 machine-id 的全过程，探索了 NoCloud `cidata.iso` 企业级全自动无人值守交付方案，完成了 100% 独立性的闭环验收。

在下一课中，我们将基于本课交付的多台独立虚拟机，学习搭建**隔离虚拟网络与 Linux 虚拟交换机（Linux Bridge）**，探索虚拟机在不同网络拓扑下的互联与二层隔离！
