Packer 自动化构建机器镜像实战:一份模板产出多平台镜像
新机器交付慢,慢的往往不是装系统本身,而是每台都要重复一串人工动作:装运行时、改内核参数、放监控 agent、对安全基线。机器一多,人和人装出来的还不完全一样。Packer 的定位就是把这件事变成“镜像即代码”:一份 HCL 模板描述“从什么基础镜像出发、装什么、怎么验收”,执行后产出一台开箱即用的机器镜像,要扩容或重建时直接从镜像启动,跳过全部重复劳动。本文拆解模板的结构(source / provisioner / build 三段),分别过一遍本地 KVM(qemu)与云平台镜像的最小示例,并交代产物规范、与 Kickstart 的分工和 CI 衔接。
前置条件
- 本地已安装 Packer(1.9+),能访问待构建平台:本地 KVM 场景需要 qemu 与 libvirt 可用,云场景需要对应云账号的访问密钥(通过环境变量注入,不要写进模板)。
- 准备好基础安装介质:ISO 的下载地址与校验和,或云平台的公共基础镜像 ID。
- 一份想固化进镜像的“标准动作清单”:要装的软件、要改的配置、要验收的项,先写成列表再动手写模板。
第一步:看懂 HCL 模板的三段结构
一份 Packer 模板(.pkr.hcl)由三类块组成:source 描述“在哪台虚拟机/什么平台上装”,provisioner 描述“装什么”,build 把两者组装起来并声明产物处理。先声明需要的插件与变量:
packer {
required_plugins {
qemu = {
version = ">= 1.0.0"
source = "github.com/hashicorp/qemu"
}
ansible = {
version = ">= 1.1.0"
source = "github.com/hashicorp/ansible"
}
}
}
variable "image_version" {
type = string
default = "1.0.0"
}
执行任何构建前先跑一次 packer init . 拉齐插件。变量统一从 var 块或外部 -var-file 注入,版本、镜像名这类每构建必变的值不要硬编码在 source 里。
第二步:本地 KVM(qemu)的最小示例
qemu 构建器从 ISO 全新安装一台虚拟机,再把磁盘固化为镜像文件,适合产出内网虚拟化平台的模板镜像。关键在启动命令(boot_command)——它模拟键盘把安装入口与 Kickstart 地址敲进去:
source "qemu" "rocky-base" {
iso_url = "https://example.com/iso/Rocky-9.iso"
iso_checksum = "sha256:请替换为官方校验和"
disk_size = "40G"
format = "qcow2"
accelerator = "kvm"
http_directory = "http"
boot_command = ["<tab> inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg<enter>"]
boot_wait = "10s"
ssh_username = "root"
ssh_password = "请替换为安装口令"
ssh_timeout = "30m"
shutdown_command = "/sbin/shutdown -hP now"
vm_name = "rocky-base-${var.image_version}.qcow2"
output_directory = "output/rocky-base"
}
几个字段的口径:http_directory 让 Packer 内置起一个小 HTTP 服务托管 ks.cfg,安装程序从这里取无人值守配置;ssh_timeout 要给足,装系统加首次启动经常超过十分钟;shutdown_command 必须能干净关机,否则磁盘镜像可能不一致。Kickstart 在这里负责操作系统层面的事:分区、选包、建初始账号;Packer 负责的是编排整个构建流程,两者分工后文还会展开。
第三步:云镜像的最小示例
云平台构建器(如 amazon-ebs、alicloud-ecs、openstack 等)不需要从 ISO 装系统,而是从公共基础镜像拉起一台临时实例,装完软件后打成自定义镜像再释放实例。结构上和 qemu 共用 provisioner,只有 source 换掉:
source "amazon-ebs" "base" {
region = "cn-north-1"
source_ami_filter {
filters = {
name = "almalinux-9-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["764195469643"]
most_recent = true
}
instance_type = "t3.micro"
ssh_username = "almalinux"
ami_name = "base-linux-{{timestamp}}"
tags = {
BaseVersion = var.image_version
BuiltBy = "packer"
}
}
云场景的要点:用 filter 按名字与属主锁定基础镜像,避免拿到被污染的同名镜像;密钥一律走环境变量(AWS 的 AWS_ACCESS_KEY_ID 等)或实例角色,不要写进模板文件;给产物打齐标签(版本、构建时间、Git 提交),后续治理全靠它们。
第四步:组织 provisioner
provisioner 写在 build 块里,按声明顺序自上而下执行。推荐的组织次序是:先传文件(file),再跑脚本(shell),最后用 Ansible 做结构化配置。简单粗暴的初始化动作用 shell 一把梭;涉及多文件、多状态的配置(用户、服务、基线项)交给 Ansible,复用现成的 role,不要在 shell 里堆几百行。
build {
sources = ["source.qemu.rocky-base", "source.amazon-ebs.base"]
provisioner "file" {
source = "files/motd"
destination = "/etc/motd"
}
provisioner "shell" {
inline = [
"dnf -y update --security",
"dnf -y install qemu-guest-agent cloud-init",
"systemctl enable qemu-guest-agent cloud-init"
]
}
provisioner "ansible" {
playbook_file = "ansible/base.yml"
user = "root"
}
post-processor "manifest" {
output = "manifest.json"
}
}
同一套 provisioner 同时挂到 qemu 与云两个 source 上,就是“一处配置、多平台产出”的关键:基础镜像口径一致,差异只留在 source 层。另外两条纪律:provisioner 里不要留任何真实口令与密钥(镜像会被分发,写进去等于公开);收尾动作(清 yum 缓存、清日志、清机器标识、删临时 SSH 公钥)放在最后一个 shell provisioner 里统一做,保证镜像“干净”。
第五步:产物规范与 CI 衔接
产物治理在构建之前就要定好:一是命名规范,镜像名带上系统、用途与版本或时间戳(如 base-linux-1.4.0-20261108),看名字就知道新旧;二是版本留痕,开启 manifest 后处理器把每次构建的产物 ID 与模板版本写成 JSON 归档,排障时能反查镜像的来源;三是生命周期,老镜像定期下线删除——云快照持续计费,无人治理半年就能堆出一笔账单。
与 Kickstart/云镜像定制的分工再明确一次:Kickstart(以及 preseed、cloud-init 的安装期配置)解决“操作系统怎么无人值守装出来”,Packer 解决“装完之后装什么、怎么验收、产物怎么管理与多平台复用”。已经有成熟 Kickstart 的团队接入 Packer 时,保留 ks 文件原样,由 boot_command 指过去即可,不需要重写。
CI 衔接很轻量:流水线里加三步——packer fmt -check 管格式、packer validate 管语法与变量、模板或 ansible 目录变更时触发 packer build 并把 manifest.json 作为制品归档。镜像构建耗时以十分钟计,不要放在每次提交上跑,按变更路径触发或夜间定时构建更合适。
packer init .
packer fmt -check .
packer validate -var "image_version=1.4.0" .
packer build -var "image_version=1.4.0" .
验证
- 用新镜像启动一台测试机,能正常开机、SSH 登录,预装软件与基线项逐项核对无误。
cloud-init status在首启后为 done(qcow2 产物);云镜像则确认实例元数据与标签齐全。- manifest.json 中记录了本次构建的产物 ID 与镜像名,与平台控制台里看到的一致。
- 镜像里不含构建期的临时账号、SSH 公钥与真实口令(抽查 /root/.ssh 与 /etc/shadow 的异常项)。
常见问题
构建卡在 “Waiting for SSH to become available”。这是最高频的故障,按四层排查:网络层,qemu 场景确认虚拟机确实拿到了 IP、http_directory 的端口没被防火墙拦;安装层,打开 VNC/控制台看安装是不是卡在某个交互界面(多半是 boot_command 敲错或 ks 文件取不到);账号层,ssh_username/password 与 ks 里建的账号是否一致;时间层,慢盘或大批量更新时把 ssh_timeout 适当调大。先看得见安装画面再调参数,盲猜只会越改越乱。
provisioner 顺序导致的莫名失败。典型如 ansible 里要改某个配置文件,但 file provisioner 还没把它传上去;或 shell 里调用了后面才安装的命令。记住 build 块严格按书写顺序执行,把“传文件 → 装软件 → 改配置 → 清理收尾”排成固定次序,新加步骤先想清楚它依赖什么、产出被谁依赖。
构建中断后残留一地资源。云构建失败时,临时实例一般会被自动回收,但临时密钥对、安全组、快照偶尔会残留,个别构建器还有专门的清理开关;qemu 场景则会留下半成品的 output 目录。处理办法:构建账号用独立的、权限受限的身份,定期按 BuiltBy 之类的标签巡检并清理无主资源;本地 output 目录在 CI 里构建后即归档删除,不要长期堆在构建机上。
同一模板在本地能跑、CI 里失败。多半是插件或 Packer 版本不一致。把版本锁进模板的 required_plugins 与 CI 镜像里,构建前打印 packer version 留痕,版本问题在日志里一眼可见。
小结
Packer 的模板就三段:source 决定在哪装,provisioner 决定装什么,build 负责组装与产物。真正让它产生复利的是纪律:一套 provisioner 多平台复用、产物命名与版本留痕、老镜像定期清理、构建进 CI 按变更触发。把这四条守住,机器交付就从“照着文档敲半天”变成“选个镜像开机”,而且每台的口径都可追溯到同一次构建。







