无垠之码

深度剖析代码之道


packer-工具使用

Packer是hashicorp公司开源的虚拟机镜像构建工具,与之类似的工具还有OpenStack diskimage-builder、AWS EC2 Image Builder,但后者只支持自家的云平台,Packer更强调平台无关性与统一的镜像构建流程,更关注系统级与基础环境的可复现构建,强调不可变的基础设施(Immutable Infrastructure)。Packer最典型的用途是制作VM镜像,如AMI-Amazon Machine Image、QCOW2、VMDK等,同时也支持Docker镜像构建,能够覆盖主流公有云、私有云以及混合云环境下的镜像构建需求,这种预制镜像Golden Image可以缩短扩容时间Auto-scaling毫秒级启动,并确保测试环境与生产环境的操作系统底座完全一致,消除"Works on my machine"的问题。

0.Packer安装


export HASHICORP_URL=https://apt.releases.hashicorp.com
wget -O- ${HASHICORP_URL}/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] ${HASHICORP_URL} $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install packer

1.基础命令


Packer和Terraform一样使用申明式语言编写(题外话:相较于命令式语言,其描述目标状态,可读性更强,易实现幂等性,易于维护、版本控制,但抽象高,调试困难,对复杂流程不友好,强依赖执行引擎能力)

peter@peter-Legion-Y9000P-IAH7H:~$ packer --help
Usage: packer [--version] [--help] <command> [<args>]

Available commands are:
    build           build image(s) from template                                        // 执行构建任务
    console         creates a console for testing variable interpolation                // 调试使用
    fix             fixes templates from old versions of packer
    fmt             Rewrites HCL2 config files to canonical format                      // 文件格式化
    hcl2_upgrade    transform a JSON template into an HCL2 configuration                // 将json结构的配置,转换为hcl语言
    init            Install missing plugins or upgrade plugins                          // 插件的安装和升级
    inspect         see components of a template
    plugins         Interact with Packer plugins and catalog
    validate        check that a template is valid                                      // 验证语法正确性           
    version         Prints the Packer version

2.简单案例


Packer通过读取并应用HCL模板文件中定义的配置来完成构建,而HCL模板以简洁的方式刻画生成构建产物所需的过程。Packer build命令接受一个参数,当参数为文件夹时,其下所有以.pkr.hcl和.pkr.json后缀的文件都将被使用HCL2格式解析。当参数是单独文件时,若文件后缀为.pkr.hcl或者.pkr.json都使用HCL2 schema解析器解析,对于不符合上述命名规则的情况,为了兼容历史配置,Packer将采用旧版仅支持JSON的配置解析方式。

HCL的语法在文章Terraform从入门到放弃中已经讲过,这里不再赘述,下面直接展示一个Packer的简单样例。

packer {
  required_plugins {
    virtualbox = {
      version = "~> 1"
      source  = "github.com/hashicorp/virtualbox"
    }
    vagrant = {
      version = "~> 1"
      source  = "github.com/hashicorp/vagrant"
    }
    git = {
      source  = "github.com/ethanmdavidson/git"
      version = ">= 0.6.5"
    }
  }
}

data "git-repository" "cwd" {}

# export VAGRANT_CLOUD_TOKEN="$(hcp auth print-access-token)"
variable "cloud_token" {
  type      = string
  sensitive = true
  default   = "${env("VAGRANT_CLOUD_TOKEN")}"
}

variable "user" {
  type    = string
  default = "vagrant"
}

variable "password" {
  type    = string
  default = "vagrant"
}

variable "cpus" {
  type    = number
  default = 2
}

variable "memory" {
  type    = number
  default = 2048
}

variable "additional_packages" {
  type        = list(string)
  description = "Additional packages to install."
  default     = []
}

locals {
  build_version = data.git-repository.cwd.head

  data_source = {
    "/meta-data" = file("../../../data/meta-data")
    "/user-data" = templatefile("../../../data/user-data.pkrtpl.hcl", {
      user                = var.user
      password            = bcrypt(var.password)
      additional_packages = var.additional_packages
    })
  }
}

source "virtualbox-iso" "ubuntu2204" {
  guest_os_type = "Ubuntu_64"
  iso_url       = "https://releases.ubuntu.com/22.04/ubuntu-22.04.5-live-server-amd64.iso"
  iso_checksum  = "file:https://releases.ubuntu.com/22.04/SHA256SUMS"

  http_content = local.data_source
  boot_wait    = "5s"
  boot_command = [
    "<wait3s>c<wait3s>",
    "linux /casper/vmlinuz console=ttyS0 console=tty0 quiet autoinstall net.ifnames=0 biosdevname=0 \"ds=nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/\" ",
    "<enter><wait5s>",
    "initrd /casper/initrd",
    "<enter><wait5s>",
    "boot",
    "<enter>"
  ]

  communicator     = "ssh"
  ssh_username     = "${var.user}"
  ssh_password     = "${var.password}"
  ssh_timeout      = "30m"
  shutdown_command = "echo '${var.password}' | sudo -S shutdown -P now"

  headless          = true
  vrdp_bind_address = "0.0.0.0"
  vrdp_port_min     = "5918"
  vrdp_port_max     = "5918"

  vboxmanage = [
    ["modifyvm", "{{.Name}}", "--memory", "${var.memory}"],
    ["modifyvm", "{{.Name}}", "--cpus", "${var.cpus}"],
    ["modifyvm", "{{.Name}}", "--uart1", "0x3F8", "4"],
    ["modifyvm", "{{.Name}}", "--uartmode1", "file", abspath("${path.root}/packer.log")],
    ["modifyvm", "{{.Name}}", "--description", local.build_version]
  ]
}

build {
  sources = [
    "source.virtualbox-iso.ubuntu2204"
  ]
  provisioner "shell" {
    scripts = [
      "../../../data/ubuntu-software-preinstall.sh"
    ]
    environment_vars = [
      "PACKER_BUILDER_TYPE=VAGRANT"
    ]
  }
  post-processors {
    post-processor "vagrant" {
      keep_input_artifact = false
      provider_override   = "virtualbox"
    }
    post-processor "vagrant-cloud" {
      access_token = "${var.cloud_token}"
      architecture = "amd64"
      version      = "0.0.5"
      box_tag      = "diyao/ubuntu2204"
    }
  }
}

上述配置主要由packer、source和build三类块构成。其中,source块用于定义构建插件的具体配置,一旦声明,该配置即可被build构建块引用,并可在build块中进一步细化。 build块包含builders、provisioners和post-processors的特定组合配置,用于生成一个具体的镜像构建产物。嵌套在build块中的provisioner块用于定义provisioner插件的相关配置。Provisioner是Packer的组成组件之一,负责在运行中的机器被制作为静态构建产物之前完成软件的安装与配置,是使构建产物具备实际可用性的核心环节,其典型实现包括shell 脚本、Chef、Puppet等。同样嵌套在build块中的post-processors块主要用于定义后处理插件及其执行序列,负责对构建产物进行压缩、上传、转换格式等后续处理操作。packer块则用于声明构建所依赖的Packer核心及其插件的来源与版本要求。另外packer语法中还支持定义局部变量的locals块,申明外部变量的variable块,用于执行计算或查询外部系统中的只读数据如基础镜像、元数据等,并供source作为构建输入使用的data块。

注意相关的插件使用教程,需要在packer的官方网站查阅(https://developer.hashicorp.com/packer/integrations),使用packer build的debug选项对构建过程展开调试,PACKER_LOG=1环境变量可以打印更多packer执行日志。

#cloud-config
autoinstall:
  version: 1
  identity:
    hostname: ubuntu
    realname: diyao
    username: ${user}
    password: ${password} 
  ssh:
    install-server: true
    allow-pw: true
  apt:
    primary:
      - arches: [default]
        uri: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
  late-commands:
    - curtin in-target -- sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="net.ifnames=0 biosdevname=0"/' /etc/default/grub
    - curtin in-target -- update-grub
    - curtin in-target -- echo "autoinstall execution complete!"
  timezone: Asia/Shanghai
  packages:
    - openssh-server
    - vim
%{ for package in additional_packages ~}
    - ${package}
%{ endfor ~}
  user-data:
    users:
      - name: ${user}
        sudo: ALL=(ALL) NOPASSWD:ALL
        groups: sudo
        shell: /bin/bash
    chpasswd:
      expire: false
      users:
        - {name: root, password: $6$VaET/p4jH3iSgN2J$bwEHdqTa9AS2GbaMpq3MPgu3hwqyvBzG.Mk.4gtZJbWUJp/GAnljj7s6Z4M2LtQgDhi6v2ZtfPo4Ktq4r.QtN0}
#!/bin/bash

export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a

function change_apt_sources() {
	echo "Changing Ubuntu APT sources to a faster mirror..."
	UBUNTU_CODENAME=$(lsb_release -cs)

	sudo tee /etc/apt/sources.list >/dev/null <<EOF
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse
EOF
	echo "APT sources updated."
}

function install_software() {
	echo "Installing essential software packages..."
	sudo -E apt-get install -y \
		build-essential \
		net-tools \
		curl \
		git \
		vim \
		wget \
		gdb \
		golang \
		unzip
	echo "Software installation complete."
	echo "Cleaning up..."
	sudo -E apt-get clean
	sudo rm -rf /var/lib/apt/lists/*
}

function setup_vagrant_ssh() {
	echo "Setting up vagrant ssh login..."
	sudo mkdir -p /home/vagrant/.ssh
	sudo chmod 700 /home/vagrant/.ssh
	sudo wget --no-check-certificate 'https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub' -O /home/vagrant/.ssh/authorized_keys
	sudo chmod 600 /home/vagrant/.ssh/authorized_keys
	sudo chown -R vagrant:vagrant /home/vagrant/.ssh
	echo "Vagrant ssh login setup complete."
}

function cleanup_aws_build_artifacts() {
	echo "Cleaning up aws build artifacts..."
	sudo rm -f /root/.ssh/authorized_keys
	sudo rm -f /home/ubuntu/.ssh/authorized_keys
}

change_apt_sources
sudo -E apt-get update
install_software

if [ "$PACKER_BUILDER_TYPE" = "VAGRANT" ] ; then
	setup_vagrant_ssh
fi

if [ "$PACKER_BUILDER_TYPE" = "AWS" ] ; then
	cleanup_aws_build_artifacts
fi

在本示例中,virtualbox-iso插件基于Ubuntu 22.04 Server的镜像,利用autoinstall实现无人值守安装,provisioner阶段执行ubuntu-software-preinstall.sh脚本完成系统配置,构建完成后,使用vagrant post-processor将虚拟机转换为Vagrant box格式,并最终通过vagrant-cloud插件上传并发布至Vagrant Cloud。开发者可以方便的使用如下代码,使用该ubuntu2204实例

vagrant init diyao/ubuntu2204
vagrant up

3.企业级构建

上述vagrant的packer构建案例适用于个人开发者使用。在企业环境中,同样可以使用Packer的vSphere插件构建ESXi系统模板,生成可供Terraform部署的VM镜像模板。

packer {
  required_plugins {
    vsphere = {
      version = "~> 1"
      source  = "github.com/hashicorp/vsphere"
    }
    git = {
      source  = "github.com/ethanmdavidson/git"
      version = ">= 0.6.5"
    }
  }
}

data "git-repository" "cwd" {}

variable "user" {
  type    = string
  default = "peter"
}

variable "password" {
  type      = string
  sensitive = true
  default   = "625886"
}

variable "cpus" {
  type    = number
  default = 2
}

variable "memory" {
  type    = number
  default = 2048
}

variable "additional_packages" {
  type        = list(string)
  description = "Additional packages to install."
  default     = []
}

locals {
  build_version = data.git-repository.cwd.head
  data_source = {
    "/meta-data" = file("../../../data/meta-data")
    "/user-data" = templatefile("../../../data/user-data.pkrtpl.hcl", {
      user                = var.user
      password            = bcrypt(var.password)
      additional_packages = var.additional_packages
    })
  }
}

source "vsphere-iso" "ubuntu2204" {
  vcenter_server      = "vsphere.diyao.me"
  username            = "administrator@vsphere.local"
  password            = "Pyy@625886"
  insecure_connection = true
  datacenter          = "Datacenter-0"
  datastore           = "datastore2"

  convert_to_template = true
  guest_os_type       = "ubuntu64Guest"
  iso_paths = [
    "[datastore1] images/ubuntu-22.04.5-live-server-amd64.iso"
  ]
  iso_checksum = "file:https://releases.ubuntu.com/22.04/SHA256SUMS"

  http_content = local.data_source
  boot_wait    = "5s"
  boot_command = [
    "<wait3s>c<wait3s>",
    "linux /casper/vmlinuz quiet autoinstall net.ifnames=0 biosdevname=0 \"ds=nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/\" ",
    "<enter><wait5s>",
    "initrd /casper/initrd",
    "<enter><wait5s>",
    "boot",
    "<enter>"
  ]

  communicator     = "ssh"
  ssh_username     = "${var.user}"
  ssh_password     = "${var.password}"
  ssh_timeout      = "30m"

  host    = "192.168.5.32"
  vm_name = "packer-ubuntu2204-template"
  notes   = local.build_version
  RAM     = var.memory
  CPUs    = var.cpus
  storage {
    disk_size             = 20000
    disk_thin_provisioned = true
  }
  network_adapters {
    network      = "LAN0"
    network_card = "vmxnet3"
  }
}

build {
  sources = [
    "source.vsphere-iso.ubuntu2204"
  ]
  provisioner "shell" {
    scripts = [
      "../../../data/ubuntu-software-preinstall.sh"
    ]
  }
}

4.踩坑避雷


  1. 官方Vagrant box为实现开箱即用的开发体验,通常会预先创建vagrant用户,并注入与Vagrant 默认insecure_private_key配对的SSH公钥。如果自定义box未包含该公钥,将导致Vagrant默认SSH登录流程失败,例子中在ubuntu-software-preinstall.sh完成公钥的导入工作

    为什么官方box都包含vagrant用户和公钥?

    Vagrant在启动虚拟机时,默认使用用户名vagrant,并通过本地的~/.vagrant.d/insecure_private_key进行SSH登录。因此,所有官方及社区标准box都会在镜像中预先创建vagrant用户,并将与该私钥配对的公钥写入/home/vagrant/.ssh/authorized_keys。这一设计的目的是实现开箱即用的开发体验,使用户无需额外配置SSH 用户名、密码或密钥即可直接使用vagrant up和vagrant ssh。如果自定义box未包含该公钥,则会导致Vagrant默认SSH登录失败。

  2. 使用vagrant init diyao/ubuntu2204 –box-version 0.0.2会在Vagrantfile中锁定box的版本约束,导致后续执行vagrant box update时不会拉取更新版本,除非手动修改或移除该版本限制。

  3. Ubuntu镜像在无人值守安装autoinstall过程中,Packer会在构建阶段临时启动一个HTTP服务,用于向安装器提供NoCloud数据源中的meta-data和user-data文件。因此,虚拟机在安装阶段必须具备可用的网络接口。如果未为虚拟机配置网卡,或网络未成功初始化,Subiquity安装器将无法从http://{{ .HTTPIP }}:{{ .HTTPPort }}/拉取autoinstall配置,导致安装流程卡住或退回交互式安装界面。在使用 virtualbox-iso、vmware-iso、qemu等builder时,应显式或隐式确保虚拟机具备至少一块可用网卡

  4. 在调试Packer模板时,建议同时启用PACKER_LOG=1并使用packer build -on-error=ask。这样即使在SSH连接或自动化安装阶段出现错误,也可以在构建失败后保留虚拟机实例,手动登录检查网络、cloud-init 日志和系统状态,大幅提升问题定位效率。

参考文献

  1. https://developer.hashicorp.com/packer/docs
  2. https://portal.cloud.hashicorp.com/
  3. https://github.com/vmware/packer-examples-for-vsphere
  4. https://ubuntu.com/server/docs/install/autoinstall
comments powered by Disqus