使用 Packer 高效地组织 QEMU 映像基础设施





5.00/5 (4投票s)
探索一种轻松构建和组织 QEMU 映像的方法。
问题
让我们尝试解决以下问题:我们需要为我们不支持 VM 快照的内部云基础设施构建所有流行 Linux 发行版的 qcow2 镜像。这些镜像主要用于测试,并且可能随着新测试的添加和旧测试的淘汰而发生变化。如果这些镜像还可以用于其他目的,那就太好了。
技术
Packer 是一个非常流行的工具,通常用于构建几乎所有东西的镜像。还有一个用于构建 qemu 镜像的插件。(https://developer.hashicorp.com/packer/plugins/builders/qemu)。
通常人们使用 Ansible 来安装包、依赖项,以及对 Linux 发行版进行一些操作。最后,使用 Python 脚本来处理任何类型的 API,为现有工具创建包装器非常方便。
因此,让我们将这三个工具结合到一个可行的解决方案中,以帮助我们处理已陈述的问题。
解决方案
首先,我们需要一个基本的 packer 配置文件,可以用于构建所有镜像。这是我构思的内容:
variable "iso_checksum" {
type = string
default = ""
}
variable "iso_url" {
type = string
default = ""
}
variable "boot_command" {
type = list(string)
default = [""]
}
variable "name" {
type = string
default = ""
}
variable "cpus" {
type = string
default = "30"
}
variable "memory" {
type = string
default = "8192"
}
variable "datastore" {
type = string
default = "108"
}
variable "disk_size" {
type = string
default = "12G"
}
variable "disk_image" {
type = bool
default = false
}
variable "deploy_image_params" {
type = string
default = ""
}
variable "playbook" {
type = string
default = ""
}
variable "AWS_ACCESS_KEY_ID" {
type = string
default = "${env("AWS_ACCESS_KEY_ID")}"
}
variable "AWS_SECRET_ACCESS_KEY" {
type = string
default = "${env("AWS_SECRET_ACCESS_KEY")}"
}
source "qemu" "qemu_source" {
boot_command = var.boot_command
boot_wait = "10s"
disk_discard = "unmap"
disk_interface = "virtio-scsi"
disk_size = var.disk_size
disk_image = var.disk_image
format = "qcow2"
http_directory = "${path.root}"
iso_checksum = var.iso_checksum
iso_url = var.iso_url
output_directory = "${path.root}/output/${var.name}"
shutdown_command = "echo 'my-fav-username'|sudo -S /sbin/halt -h -p"
ssh_handshake_attempts = 1000
ssh_password = "my-fav-ssh-password"
ssh_port = 22
ssh_username = "my-fav-username"
ssh_wait_timeout = "10000s"
net_device = "virtio-net"
vm_name = "${var.name}.qcow2"
cpus = var.cpus
accelerator = "kvm"
disk_compression = true
headless = true
vnc_bind_address = "0.0.0.0"
qemuargs = [[ "-cpu", "host" ]]
memory = var.memory
}
build {
sources = ["source.qemu.qemu_source"]
provisioner "ansible-local" {
extra_arguments = [
"--extra-vars", "ansible_become_pass=my-fav-username"
]
playbook_file = "${path.root}/${var.playbook}"
playbook_dir = "${path.root}/ansible"
}
post-processor "shell-local" {
environment_vars = [
"EXEC_DIR=${path.root}/post-processors",
"IMAGE=${path.root}/output/${var.name}/${var.name}.qcow2",
"DEPLOY_IMAGE_PARAMS=${var.deploy_image_params}",
"AWS_ACCESS_KEY_ID=${var.AWS_ACCESS_KEY_ID}",
"AWS_SECRET_ACCESS_KEY=${var.AWS_SECRET_ACCESS_KEY}"
]
execute_command = ["/bin/bash", "-c", "{{ .Vars }} {{.Script}}"]
inline = [
"$EXEC_DIR/deploy-image.py -i $IMAGE $DEPLOY_IMAGE_PARAMS"
]
name = "cloud_deploy"
}
}
文件中定义的变量可以在 Packer 的实际执行过程中重新定义。请注意 qemu 构建器插件中定义的变量,位于 block source "qemu" "qemu_source"
下,其中最重要的是 boot_command
,它在虚拟机启动时使用。 以及 iso_url
,它指定了虚拟机镜像的位置。关于剩余变量的更多描述,请参阅此链接:https://developer.hashicorp.com/packer/plugins/builders/qemu。其他用户定义的变量定义在 qemu 构建器块的上方。我们将在 Packer 执行期间使用它们来重新定义特定镜像的行为。
接下来,让我们讨论 provisioner "ansible-local"
和 post-processor "shell-local"
块。'ansible-local'
provisioner 负责在 Linux 发行版安装后立即运行 Ansible 脚本。其目的是安装所有必需的依赖项。如您所见,实际的 playbook 可能会有所变动。此外,请注意这是一个 'local'
provisioner,它假定 Ansible 已经在安装 Linux 发行版后在虚拟机中安装好了。这可以通过使用 seed 文件来完成,我稍后将对此进行更详细的解释。
post-processor 块在 provisioner 完成其任务并且镜像准备好使用后被调用。我们将使用此 post-processor 来执行一个 Python 脚本,该脚本将新构建的镜像上传到我们的内部云。
好的,让我们继续我们解决方案的下一个组件。我们现在需要为特定的发行版定义实际用户定义变量的特定配置。这是 Ubuntu 22.04 的配置:
iso_url = "http://releases.ubuntu.com/22.04/ubuntu-22.04-live-server-amd64.iso"
iso_checksum = "84aeaf7823c8c61baa0ae862d0a06b03409394800000b3235854a6b38eb4856f"
name = "ubuntu-22.04-base"
disk_size = "64G"
playbook = "ubuntu/22.04/base/provision.yml"
deploy_image_params = "inner_cloud -bc base_cloud_config.json"
boot_command = [
"<esc><esc><esc><esc>e<wait>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"<del><del><del><del><del><del><del><del>",
"linux /casper/vmlinuz --- autoinstall ds=\"nocloud-net;
seedfrom=http://{{ .HTTPIP }}:
{{ .HTTPPort }}/ubuntu/22.04/base/preseed/\"<enter><wait>",
"initrd /casper/initrd<enter><wait>",
"boot<enter>",
"<enter><f10><wait>"
]
playbook
参数指向基本 HCL Packer 配置的 provisioner 块中使用的 Ansible playbook 文件的路径。由于我们使用了 'ansible-local' provisioner,Packer 会自动将此 playbook 复制到虚拟机中。
deploy_image_params
参数 指向传递给基本 HCL 配置的 post-processor 块中,用于我们负责上传已构建镜像的自定义 Python 脚本的参数。您可以查看基本配置文件以查找这些参数的使用情况。
看似复杂的 boot_command
参数表示在虚拟机启动时执行的一系列命令,用于启动 Linux 发行版的安装过程。对于不同的发行版,启动命令会有所不同。请注意启动命令中的以下行:
"linux /casper/vmlinuz --- autoinstall ds=\"nocloud-net;
seedfrom=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ubuntu/22.04/base/preseed/\"<enter><wait>",
为了安装 Ubuntu 22.04,我们使用一个通过 HTTP 提供的 seed 文件。在提供的路径中,您可以看到该文件的位置。它存储在本地文件系统中,路径从 Packer 工作目录开始。seed 文件包含镜像安装过程所需的必要信息。
#cloud-config
autoinstall:
version: 1
apt:
geoip: true
disable_components: []
preserve_sources_list: false
primary:
- arches: [amd64, i386]
uri: http://us.archive.ubuntu.com/ubuntu
- arches: [default]
uri: http://ports.ubuntu.com/ubuntu-ports
early-commands:
- sudo systemctl stop ssh
locale: en_US
keyboard:
layout: us
identity:
hostname: jammy
username: my-fav-username
password: "$6$rounds=4096$Uua67h5t4Fi2QpL6$0Y8f4YDDpTlwC.
02F3WvWmKBq305uV0cTMXzugpyLYNCwYziCkfE0mIHiBTbc.ZgJhxTp3uonZ22yUtNyHv9x1"
ssh:
install-server: true
allow-pw: true
packages:
- openssh-server
- open-vm-tools
- cloud-init
- whois
- zsh
- wget
- tasksel
- ansible
user-data:
disable_root: false
timezone: UTC
late-commands:
- sed -i -e 's/^#\?PermitRootLogin.*/PermitRootLogin yes/g'
/target/etc/ssh/sshd_config
- sed -i -e 's/^#\?PasswordAuthentication.*/PasswordAuthentication
yes/g' /target/etc/ssh/sshd_config
- echo 'vmadmin ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/my-fav-username
- curtin in-target --target=/target -- chmod 440 /etc/sudoers.d/my-fav-username
- "lvresize -v -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv"
- "resize2fs -p /dev/mapper/ubuntu--vg-ubuntu--lv"
这里的 identity:password
是使用 SHA-512 加密的字符串。Packages 块在这里是安装过程中将要安装的内容,late-commands 是安装完成后要运行的命令。请注意,这是我们安装 Ansible 的地方,它将用于我们的 'ansible-local' provisioner。实际上,我们可以在这里做很多事情,从而完全摆脱 Ansible,但使用 Ansible 可以提供更大的清晰度,并使迁移到其他镜像更容易,而 seed 文件中的这些命令特定于此特定发行版。
下一个需要考虑的项是我们将在初始安装命令完成后执行的 Ansible playbook。playbook 遵循标准格式,因此我不会在此赘述。您可以在我的 GitHub 仓库 中找到所有代码。有关实际 Ansible playbook,请参阅以下位置:images/ubuntu/22.04/base/provision.yml。此外,playbook 还引用了 Ansible roles,您可以在这里找到它们:images/ansible/roles
最后一个组件是用于将镜像部署到我们的内部云并将其保存到 S3 的 Python 脚本。这是脚本中 parse_args()
方法的一个示例:
def parse_args():
argparser = argparse.ArgumentParser()
argparser.add_argument('-i', '--image', required=True, help='path to image file')
argparser.add_argument('--s3-path', help='skip deployment to s3,
just use predefined path')
subparsers = argparser.add_subparsers()
nebula_parser = subparsers.add_parser('cloud', help='cloud specific params')
nebula_parser.add_argument
('-c', '--config', help='path to nebula image config file')
nebula_parser.add_argument('-bc', '--base-config',
help='path to nebula image base config file')
return argparser.parse_args()
此脚本的参数是从我们特定发行版的特定 HCL 文件传递的,请参阅 deploy_image_params
。关于特定于云的参数,我遵循与 Packer 参数类似的方法,即我有一个基础云配置和每个特定发行版的单独配置,其中包含覆盖的变量。通过此脚本,我们可以灵活地对镜像执行各种操作。例如,我们可以实现版本控制方案,或者从 S3 或云中删除过时的镜像。换句话说,在新镜像到达时需要执行的任何操作都可以在这里实现。
现在我们已经有了所有构建模块,让我们来看看文件结构是如何组织的:
>>> images/
>>> ansible/
>>> roles/
>>> ...
>>> post-processors/
>>> deploy-image.py
>>> ubuntu/
>>> 22.04/
>>> base/
>>> preseed/
>>> meta-data
>>> user-data
>>> custom.pkrvars.hcl
>>> provision.yml
>>> ...
>>> base.pkr.hcl
在文件结构中,您会注意到 ubuntu/22.04 下的 base 文件夹。这意味着我们可以构建基于基础镜像的其他镜像。这可以通过在 Packer qemu 构建器中使用 disk_image
参数来实现。如果 disk_image
设置为 true
,则会跳过安装过程。相反,Packer 将使用 iso_url
中指定的镜像启动虚拟机,并在其上运行 provisioner 和 post-processor。非基础镜像的配置示例:
iso_url = "base_image"
iso_checksum = "none"
disk_image = true
name = "ubuntu-22.04-jenkins-agent"
disk_size = "64G"
playbook = "ubuntu/22.04/jenkins-agent/provision.yml"
deploy_image_params = "inner_cloud -bc base_cloud_config.json"
我们没有 boot_command
,因为它是不必要的。基础镜像已经安装了 Linux 发行版。在这种情况下,我们只需添加一个自定义 Ansible playbook。iso_url
应在 Packer 的实际执行过程中提供。我使用了另一个 Python 脚本来获取此 iso_url
。我采用了特定的命名约定,因此,如果我们有一个名为 ubuntu-22.04
的发行版,则基础发行版名称将是 ubuntu-22.04-base
。S3 存储中的路径是固定的。
启动打包构建基础镜像的命令:
packer build -var-file images/ubuntu/22.04/base/custom.pkrvars.hcl base.pkr.hcl
启动打包构建非基础镜像的命令:
packer build -var iso_url=http://path_to_s3_with_base_image
-var-file images/ubuntu/22.04/jenkins-agent/custom.pkrvars.hcl base.pkr.hcl
此处未提及的另一个重要方面是 Jenkins 作业,该作业检测 images/ansible 目录中的更改,并生成一组 Packer 构建命令来重新构建受影响的镜像。为了实现这一点,我们使用另一个 Python 脚本,该脚本依赖于 playbook 和 roles 的结构。首先,我们确定更改的文件属于哪个 role,然后我们找出与该 role 相关的 playbook。最后,我们为该 playbook 所在的每个镜像生成一个 Packer 命令。
就是这样!有关实际代码,请参阅 GitHub 仓库。