65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2023 年 7 月 10 日

CPOL

6分钟阅读

viewsIcon

16760

探索一种轻松构建和组织 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 仓库

© . All rights reserved.