需求
我有一个 Proxmox VE 服务器来提供虚拟化服务,随着虚拟机装系统的次数越来越多,我发现我需要构建虚拟机模板,就像 VPS 商家那样在使用时直接克隆一个出来。不然每次装系统太累了。
于是我手搓了好几个虚拟机模板,按照步骤安装系统,设置源,安装需要的软件等等。暂时解决了我需要立即可用的虚拟机的问题。
以上的方案在我用了一段时间后,我发现:实际使用依旧非常困难。最大的问题莫过于每次克隆出来的虚拟机都需要进行软件包升级。以及我无法使用 cloudinit 初始化虚拟机,虚拟机的主硬盘扩容困难,需要借助救援系统。
因此我打算结合我的 Drone CI 和 Packer 以及 Cloudinit 来构建一个快速可用、易于扩容的方便的镜像。
我之前写过一篇 HashiCorp vagrant 的文章:《Vagrant 入门指北》,Packer 和 Vagrant 的功能有些类似,Vagrant 面向的是本地安装的虚拟机,例如 Virtualbox,Hyper-V 等。而 Packer 是为 Cloud Stack, Open Stack, Proxmox 等提供镜像。
结合 Packer 与 Cloudinit
为什么有 Packer 负责构建镜像了,还需要使用 Cloudinit 呢?实际上二者面向的是不同步骤。当然,cloudinit 和 packer 能达成相同的目标。但是实现方式却不一样,难易程度也不一样。
举个例子:packer 构建出的更像是 docker 的基础镜像,类似于 debian 镜像,cloudinit 更像是使用这个基础镜像,然后在其上继续构建镜像。
我通常会将通用软件包的安装交给 packer,cloudinit 将会配置 ssh 密钥、用户名、网络配置等,做个性化的配置,这样使用一个模板,但是可以产生多个不同的系统配置,用于不同工作。即使单独使用任意一个工具,也能达成相同的目的,但是需要创建多个虚拟机模板,灵活性就很差了。
Why Not Cloud Images?
原因还是在于我想定制模板,即使使用一些发行版提供的 cloud images,你还是需要卸载软件,设置软件源等等。还有一些发行版并不提供这类镜像。再者,很多发行版的 cloud images 通常会进行 Nightly Build,下载下来后还是要进行一些设置才能达到方便使用。每次导入这个镜像也比较麻烦,还需要打开命令行操作。
这类镜像还有一个问题就是 swap 分区的大小没法设置,以及它们的启动方式为 Legacy BIOS 启动,而我想使用 UEFI,以提供更方便的磁盘和分区支持。
从头开始制作镜像
截至本文编写时,Packer 官方的教程是使用 docker ,构建 docker 镜像。为虚拟机平台提供镜像的教程比较少,只有一个 AWS 的教程。只具有参考价值,适合去了解packer。
总的来说构建镜像 packer 需要一些配置:
- Communicators 负责连接虚拟机的配置,一般默认使用 ssh 无需配置
- Builders 非常重要的配置,负责连接虚拟化平台,也包含有虚拟机配置信息
- Provisioners 使用 communicators 连接虚拟机,安装软件、配置系统
官方推荐使用 hcl 作为配置文件格式,这个格式和我们常见的格式不太一样,不过配置看起来非常清晰。
构建思路
配置
首先分享一个基础的为 debian 构建镜像的配置:
packer {
required_plugins {
proxmox = {
version = "~> 1"
source = "github.com/hashicorp/proxmox"
}
ansible = {
version = "~> 1"
source = "github.com/hashicorp/ansible"
}
}
}
variable "password" {
type = string
default = "12345"
}
variable "username" {
type = string
default = "root@pam"
}
source "proxmox-iso" "debian" {
boot_command = ["<down><down><enter><wait>", "<down><down><down><down><down><wait>", "e<wait>", "<down><down><down><end>", "preseed/url=https://example.com/preseed.cfg", "<f10>"]
boot_wait = "10s"
disks {
disk_size = "10G"
storage_pool = "local-lvm"
type = "virtio"
}
efi_config {
efi_storage_pool = "hdd01-vm"
efi_type = "4m"
pre_enrolled_keys = true
}
insecure_skip_tls_verify = true
iso_file = "hdd01-iso:iso/debian-12.6.0-amd64-DVD-1.iso"
network_adapters {
bridge = "vmbr0"
model = "virtio"
}
node = "hsd01"
vm_id = 10000
username = "${var.username}"
password = "${var.password}"
proxmox_url = "https://192.168.1.10:8006/api2/json"
memory = 2048
cores = 1
bios = "ovmf"
ssh_password = "123456"
ssh_timeout = "30m"
ssh_username = "vince"
template_description = "debian-12, generated on ${timestamp()}"
template_name = "debian-12"
unmount_iso = true
cloud_init = true
cloud_init_storage_pool = "hdd01-vm"
}
build {
sources = ["source.proxmox-iso.debian"]
provisioner "shell" {
scripts = ["./init.sh"]
execute_command = "echo '123456' | sudo -S env {{ .Vars }} {{ .Path }}"
pause_before = "3s"
timeout = "10s"
start_retry_timeout = "5m"
}
provisioner "ansible" {
pause_before = "3s"
playbook_file = "./playbooks/upgrade_system_playbook.yml"
extra_arguments = [
"--extra-vars", "ansible_become_pass=123456",
"--scp-extra-args", "'-O'",
]
}
provisioner "shell" {
inline = ["echo '123456' | sudo -S systemctl reboot"]
expect_disconnect = true
}
provisioner "ansible" {
pause_before = "3s"
playbook_file = "./playbooks/install_package_playbook.yml"
extra_arguments = [
"--extra-vars", "ansible_become_pass=123456",
"--scp-extra-args", "'-O'",
// "--become-password-file=./password.txt",
]
}
}
虚拟机资源
首先需要配置一个支持 UEFI 启动的虚拟机这里可以看到:
- memory = 2048
- cores = 1
- bios = “ovmf”
- disk_size = “10G”
在这个配置中的 UEFI 磁盘类似于主板上存储 EFI 信息的 flash,而不是 EFI 分区,注意不要混淆概念。
Boot command
这个配置中有趣的是 boot_command
要注意的是:在 UEFI 启动时,不能直接进入 boot 命令提示符而是需要修改 grub 的启动菜单。只需要在合适的地方加入 preseed.cfg 的 url。
packer 可以通过 api 来实现模拟用户键盘输入的效果从而达到自动构建。packer 在构建时会启动一个 http 服务器你可以直接将 preseed.cfg 通过该服务器传递给虚拟机。
SSH
ssh 中的用户名和密码需要和 preseed.cfg 文件匹配,这样才能连接到虚拟机。这个 ssh_timeout 的时间需要留充足,这是留给装系统的时间,如果你安装的包较多,或者你在一块慢速的机械硬盘上安装,那么需要适当提高超时时间。
Cloud init
为了将来使用 cloudinit 为该虚拟机提供 cloudinit 磁盘。以开启该功能,除此之外在下一步安装软件的过程中也需要安装 cloudinit 才行。
Preseed.cfg
debian 提供了示例的 preseed.cfg 这是自动化安装关键。注意你需要提供分区配置,将 /
分区放在最后。在将来扩容磁盘时,cloudinit 会自动将该分区扩展占满空闲部分。下面是一个 4G swap 和 512M EFI 分区的示例
d-i partman-auto/expert_recipe string \
unencrypted-install :: \
512 512 512 vfat \
$primary{ } $bootable{ } \
method{ efi } format{ } \
use_filesystem{ } filesystem{ vfat } \
mountpoint{ /boot } \
. \
4096 4096 4096 linux-swap \
method{ swap } format{ } \
. \
5120 5120 1000000000 ext4 \
$primary{ } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ / } \
.
Provisioner
最后的 provisioner 对于大部分内容采用 ansible 进行 provision 来保证幂等性,当然只使用 shell 也是可以的。由于可能涉及内核的升级,需要重启系统,在这一步使用了一个 shell 命令,以及期望 ssh 断开。
inline = ["echo '123456' | sudo -S systemctl reboot"]
expect_disconnect = true
在这一步安装 cloudinit,该VM就可以使用 cloudinit 了
PXE
如果跑通了上面这个流程那么可以试一下 PXE 启动,设置一下 dns 和 tftp。这样就不再需要使用iso文件了。虽然可以声明镜像的 url 不过大部分国内镜像站做了一些防护措施,会导致 packer 不能正常下载镜像。其次对于 debian 没有最新 iso 对应的 url 的固定链接。
使用 PXE 可以再次简化自动化安装流程。netboot 也是一个不错的项目。
构建与CI
hcl 的格式化使用 packer fmt filename.hcl
。接着 packer build .
这一步骤就类似于 docker build
构建镜像。等待系统安装完成后,packer 会自动将该虚拟机转换为模板。最后还可以连接 CI/CD 工具,以实现自动化构建镜像,需要用虚拟机的时候直接从模板克隆即可,时刻保证软件包是最新的。
更进一步,还可以安装好特定的应用,直接一键搭建一个应用。