一、前言

在PVE中进行测试或者部署集群时,经常需要一次性启动大量虚拟机,如果一台一台手动安装费时费力还容易出错。有没有什么法子可以自动化进行安装?有的有的,cloud-init负责配置虚拟机,terraform负责实现一键部署虚拟机。

本文使用环境配置为:

  • 虚拟化平台:PVE 9.0.6
  • 自动化运维工具:terraform
  • 系统初始化工具:cloud-init
  • 虚拟机系统:Debian 13

参考文章:

二、什么是Cloud-Init

在生产环境,尤其是云服务供应商的大规模生产环境中,如何快速启动一个已初始化的虚拟机是个很重要的问题。针对此需求,cloud-init应运而生,该方案可以快速启动多个虚拟机且同时完成IP、软件源、用户等等配置,开箱即用无需手动一台台安装虚拟机。

使用clolud-init需要有两个先决条件:

  • 虚拟机的系统镜像支持cloud-init且未启动过,即首次启动过程中会查找是否有数据源并进行自动配置
  • 虚拟机有可访问的配置数据源,将按配置初始化系统

1. 系统镜像

绝大多数发行版官方提供云镜像,或者可以使用普通的镜像手动安装cloud-init使其成为云镜像。虽然自行封装的云镜像自定义化程度更高,但是为了向后兼容性和可维护性还是推荐从基础镜像开始,一切初始化都通过cloud-init的配置实现。

在此就以Debian 13qcow2云镜像为例,将其下载到PVE主机上:

1
wget https://cloud.debian.org/images/cloud/trixie/20250924-2245/debian-13-genericcloud-amd64-20250924-2245.qcow2

创建虚拟机:

1
2
qm create 9000 --name ci-vm
qm set 9000 --scsi0 local-lvm:0,import-from=/root/debian-13-genericcloud-amd64-20250924-2245.qcow2

PS: 其实这里虚拟机的配置不重要,后面具体配置都是通过terraform进行申明。

2. 数据源配置

cloud-init数据源由一系列配置文件组成:

  • user-data.yml: 主要的配置文件
  • network-config.yml: 关于网络如ip/nameserver/dns等的定义
  • meta-data.yml: 一般包含instance id,唯一的一个机器标识符
  • vendor-data.yml: 由供应商(云)定义,类似user-data的数据,可为空

如何让虚拟机在启动过程中访问到cloud-init数据源是关键问题,不同的云供应商有不同的方式,但是基本可归结为以下几种:

  • 本地驱动器挂载

    • 将一系列配置文件打包为ISO文件并在启动前挂载到虚拟机上
    • 通过软盘驱动器挂载
  • 远程HTTP、FTP等配置获取

    • 预先在云镜像的 /etc/cloud/cloud.cfg/etc/cloud/cloud.cfg.d/内配置好数据源URL
    • 内置了某些的云服务商(AWS、AliYun等)的数据源地址

虽然获取配置方式多种多样,但是殊途同归,最终目标就是让虚拟机得到cloud-init的配置文件。

cloud-init 的可配置项目非常丰富,可以查阅官方的examples,贴出一个常用的user-data.yml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#cloud-config

# 配置主机名以及对应hosts
hostname: ci-vm
manage_etc_hosts: true

# 添加用户以及对应的密码、ssh key
user: clemon
password: pa55w0rd!
ssh_authorized_keys:
- ssh-rsa xxxxx clemon@xxx
chpasswd:
expire: False
users:
- default

# 修改apt源
apt:
primary:
- arches: [default]
uri: https://mirrors.tuna.tsinghua.edu.cn/debian/
security:
- arches: [default]
uri: https://mirrors.tuna.tsinghua.edu.cn/debian-security
# 升级所有包
package_upgrade: true

# 基础配置
keyboard:
layout: us
locale: en_US.UTF-8
timezone: Asia/Shanghai

# 安装软件包
packages:
- qemu-guest-agent
- sudo
- htop
- vim

# 运行命令
runcmd:
- ["systemctl", "enable", "qemu-guest-agent"]

# cloud-init完成后重启
power_state:
delay: now
mode: reboot
message: Rebooting machine
condition: true

使用genisoimage命令将cloud-init 配置文件打包成一个ISO文件:

1
genisoimage -output cloud-init-data.iso -volid cidata -joliet -rock user-data meta-data network-config

3. 测试

首先要将生成的cloud-init-data.iso挂载到虚拟机:

1
qm importdisk 9000 cloud-init-data.iso local-lvm

开机验证配置是否正确。这部分简单说明,只是为了了解cloud-init是如何实现的,具体的参考基于Cloud-init定制化虚拟机,作者写的非常好。并且我这里没有加入网络相关的配置,所以虚拟机是没有IP的,network-config.yml用来储存网络相关配置,但是网卡名字是启动后系统自行设置的,具体规则几何还待研究。

三、基础设施即代码Terraform

Terraform官方口号为基础设施即代码,所以理念也很清楚就是使用代码来定义一切,甚至可以一键部署上百台虚拟机。

简单来说,通过编写Terraform配置文件并Apply即可完成各种基础设施的操作,它通过Provider支持了各个云服务商、PVE等虚拟化平台、K8S等容器编排工具的操作,功能包括但不限于创建虚拟机、上传文件、开关机。

对于Terraform配置文件,简单来说分为以下几个组建:

  • provider提供操作基础设施的能力
  • resource定义一个资源,如PVE中的虚拟机
  • 模版渲染、SSH操作、文件读写等其他辅助功能

Terraform的好处显而易见,将一切的图形化、脚本化操作转变为了配置,并且可持久化跟踪资源状态(支持本地、数据库、对象存储等作为状态存储),实现基础设施的幂等、统一管理等功能特性。

1. 创建PVE用户

创建在PVE中创建用户并设置合适的权限,以供Terraform PVE Provider使用:

1
2
3
pveum role add TerraformProv -privs "Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit Pool.Allocate Sys.Audit Sys.Console Sys.Modify VM.Allocate VM.Audit VM.Clone VM.Config.CDROM VM.Config.Cloudinit VM.Config.CPU VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Migrate VM.PowerMgmt SDN.Use"
pveum user add terraform-prov@pve
pveum aclmod / -user terraform-prov@pve -role TerraformProv
  • 由于测试环境为PVE 9.0,官方移除了VM.Monitor权限,这里也移除
  • 安全起见使用Token而不是密码,因此不设置用户密码

terraform-prov@pve生成Token:

1
2
3
pveum user token add terraform-prov@pve mytokenXXX
# 取消Separated privileges,即该Token继承用户的所有权限
pveum user token modify terraform-prov@pve mytokenXXX --privsep 0

记录下返回的token_idtoken_secret,类似于:

1
2
token_id      = "terraform-prov@pve!mytokenXXX"
token_secret = "5abcdefg-0000-xxxx-aaaa-bdc160e62681"

2. Terraform创建

cloud-init 配置分割为两部分:

  • user-data.yml: 储存较为固定的通用配置,需要上传到PVE的某个目录里
  • resource "proxmox_vm_qemu"ipconfig0nameserver: 网络配置

首先需要有虚拟机模版:

1
qm template 9000

资源定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "3.0.2-rc04"
}
}
}

provider "proxmox" {
pm_api_url = "https://10.1.0.73:8006/api2/json"
pm_api_token_id = "terraform-prov@pve!mytokenXXX"
pm_api_token_secret = "5abcdefg-0000-xxxx-aaaa-bdc160e62681"
pm_tls_insecure = true
# telmate/proxmox 3.0.2-rc04 未完全适配 PVE 9.0,禁用权限检查
pm_minimum_permission_check = false
}

resource "proxmox_vm_qemu" "common-vms" {
name = "test-terraform-efi"
target_node = "pve1"
agent = 1
os_type = "cloud-init"
memory = 1024
bios = "ovmf"
boot = "order=scsi0" # has to be the same as the OS disk of the template
clone = "trixie-cloudinit" # The name of the template
scsihw = "virtio-scsi-single"
vm_state = "running"
automatic_reboot = true

cpu {
cores = 2
sockets = 1
type = "host"
}

# Cloud-Init configuration
cicustom = "user=local:snippets/user-data.yml" # /var/lib/vz/snippets/user-data.yml
nameserver = "10.10.0.254"
ipconfig0 = "ip=10.10.0.58/24,gw=10.10.0.254"
skip_ipv6 = true

# Most cloud-init images require a serial device for their display
serial {
id = 0
}

vga {
type = "serial0"
}

disks {
scsi {
scsi0 {
# We have to specify the disk from our template, else Terraform will think it's not supposed to be there
disk {
storage = "data"
# The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated
size = "16G"
}
}
scsi1 {
cloudinit {
storage = "data"
}
}
}
}

network {
id = 0
bridge = "dev"
model = "virtio"
}
}

执行terraform apply即可观察虚拟机创建。

四、其他

  • hostname定义在user-data.yml中,无法直接在Terraform中修改,可以考虑针对每个虚拟机创建各自的user-data.yml并使用Terraformterraform-data上传至PVE主机
  • 使用genericcloud模板镜像对dkms支持有问题,如果需要完整功能请用类似debian-13-generic-amd64-20250924-2245.qcow2的镜像。