Automating Golden Image builds with Packer
Foreword
Failure is success in progress.
— Albert Einstein
Let me start by admitting that I have failed at least a dozen times during the preparation of practical portion for this article. I would like to say that I love to fail, but that would be a lie.
However, over time I learned that failing is part of the process, and each failure is a valuable lesson from which I can extend my understanding. The ingredient that keeps me going is persistence and a clear vision for the result.
In this article, through practical example, I would like to present a tool called Packer. I use it for automating OS installation from upstream source ISO file. With it, you can generate identical virtual machine templates which can be quickly deployed across different hypervisors or cloud providers.
By the end of this article, you will learn and understand how Packer works and how you can use it to build a Ubuntu 23.04 Server virtual machine template (with EUFI firmware) for major popular hypervisors such as Microsoft Hyper-V, VMware Workstation Pro, and Oracle VirtualBox.
Packer
HashiCorp Packer is an open-source tool created by Mitchell Hashimoto. As stated earlier, it solves the problem of creating identical machine images for multiple platforms, such as VMs, containers, and cloud providers.
It allows developers and system administrators to automate the process of building “Golden Images” — pre-configured and ready-to-use machine images that serve as the foundation for deploying applications. By providing a consistent and reproducible image creation process, Packer helps eliminate manual configuration tasks, reduces deployment time, and ensures consistency across different environments, making it an essential tool in modern DevOps and infrastructure automation workflows.
In the next section, we take a look at where Packer sits in our overall architecture for building golden images.
Architecture
At a high level, the following components are involved during image build:
Git Repository is recommended place where to store the source artifacts such, as templates, OS installation answer files (cloud-init or kickstart or debian-preseed), and any provisioning scripts.
Template is written in JSON or HCL (HashiCorp Configuration Language) and defines the desired configuration for the VM image. The template specifies the builders, provisioners, and post-processors required for image creation. We will cover these in great detail in Template section.
Answer files and scripts can be used during OS installation and further customization after installation.
Hypervisors play a crucial role in the process of creating virtual machine template. They provide the actual environment where the installation is being carried out. Templates can be also created in a cloud platform such as (AWS or Azure).
Vagrant Cloud can be used to publish the VM template template for public use. These images can be used by Vagrant to quickly deploy a virtual machine.
In the next section, we take a closer look at sample Packer template file that was used to build the maroskukan/ubuntu2304 box file.
Template
Packer template file is a configuration file written in JSON (legacy) or HCL (preferred) that acts as a blueprint for creating machine images using Packer. It provides a structured and declarative approach to define the desired state and instructions for the image creation process.
The template file consists of sections such as variables, sources, communicators, builders, provisioners, and post-processors, each serving a specific purpose. We describe each of these in the following paragraphs.
💡Tip: If you want to quickly review the Packer Template described in this section have a look at the Ubuntu 23.04 EUFI template.
Packer
The packer section serves as as the foundation for defining the overall Packer-specific configuration, such as minimum required version and required plugins which would be installed using packer init
command.
packer {
required_version = ">= 1.7.0"
required_plugins {
hyperv = {
version = ">= 1.1.1"
source = "github.com/hashicorp/hyperv"
}
vmware = {
version = ">= 1.0.8"
source = "github.com/hashicorp/vmware"
}
}
}
💡Tip: The packer section is optional, however I recommend to include it in the template as it helps to simplify plugin installation process.
Locals
The locals section allows for the declaration of variables, serving as internal references within the template.
Below is a code snippet where we use to store the current date, which can be used during export for box version value.
locals {
version = formatdate("YYYY.MM.DD", timestamp())
}
Variable
The variable section allows for the definition of user-defined variables, which can be used to provide inputs or configuration values during the build process. It provides a way to parameterize the template and make it more flexible and customizable based on different use cases or environments.
variable "name" {
type = string
default = "ubuntu2304"
}
variable "cpus" {
type = string
default = "2"
}
variable "memory" {
type = string
default = "2048"
}
variable "disk_size" {
type = string
default = "81920"
}
variable "iso_urls" {
type = list(string)
default = ["iso/ubuntu-23.04-live-server-amd64.iso", "https://releases.ubuntu.com/23.04/ubuntu-23.04-live-server-amd64.iso"]
}
variable "iso_checksum" {
type = string
default = "c7cda48494a6d7d9665964388a3fc9c824b3bef0c9ea3818a1be982bc80d346b"
}
💡Tip: You can use the packer console box-config.pkr.hcl
to test variable interpolation and evaluation. When in console retrieve variable value by typing "${local.version}”
or "${var.disk_size}
.
Sources
The source section is used to define the source image or artifact that will be used as the base for building the machine image. It specifies where to find the source image and provides the necessary information to retrieve or access it during the image creation process.
Different sources can be defined based on the platform or technology being used.
hyperv-iso
The hyperv-iso builder plugin is required when we want to create a new virtual machine template for Hyper-V. The starting point for this builder is an Operating System (OS) ISO file.
💡Tip: Before we can use this plugin, we need to install it explicitly using packer plugins install github.com/hashicorp/hyperv
or if you using Packer v1.7+ and the template contains the required_plugins
section we can use packer init
to install all required plugins instead.
There are some well known keys common for all builders, these include:
boot_command
— contains the keystroke sequence that will be typed when the virtual machine boots. This is heavily depended on what the installer expects to be typed in order to carry out the automated installation.boot_wait
— defines time in seconds that Packer waits when VM is powered on and boot commands are entered. Sometimes youcommunicator
— defines the means how Packer will talk to virtual machine once it installs the OS. For Linux, this is usually set tossh
for Windows, it would bewinrm
.
The hyperv-iso builder specific keys are as follows:
generation
— defines whether we want to build an Generation 1 (BIOS-based firmware) or Generation 2 (EUFI-based firmware). The selection will heavily influence which features are available.enable_secure_boot
andsecure_boot_template
are used together to further adjust the EUFI firmware behavior. For Linux VMs theMicrosoftEUFICertificateAuthority
works well.configuration_version
defines the compatibility level for Hyper-V Hosts. To increase compatibility I recommend to build for version that is not the latest and greatest if you don’t need a specific feature.
💡Tip: You can retrieve the list of supported configuration versions using the Get-VMHostSupportedVersion
PowerShell command.
The following code snipped displays the entire configuration section for this particular builder:
source "hyperv-iso" "efi" {
boot_command = [
"c",
"linux /casper/vmlinuz autoinstall quiet net.ifnames=0 biosdevname=0 ",
"ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/' --- <enter><wait>",
"initrd /casper/initrd<enter><wait>",
"boot<enter>"
]
boot_wait = "5s"
communicator = "ssh"
vm_name = "packer-${var.name}"
cpus = "${var.cpus}"
memory = "${var.memory}"
disk_size = "${var.disk_size}"
iso_urls = "${var.iso_urls}"
iso_checksum = "${var.iso_checksum}"
headless = false
http_directory = "http"
ssh_username = "vagrant"
ssh_password = "vagrant"
ssh_port = 22
ssh_timeout = "3600s"
enable_dynamic_memory = false
enable_secure_boot = true
guest_additions_mode = "disable"
switch_name = "Default switch"
generation = "2"
secure_boot_template = "MicrosoftUEFICertificateAuthority"
configuration_version = "10.0"
output_directory = "builds/${var.name}-${source.name}-${source.type}"
shutdown_command = "echo 'vagrant' | sudo -S shutdown -P now"
}
vmware-iso
The vmware-iso is required when we want to create a new virtual machine template for VMware Workstation. The starting point for this builder is an Operating System (OS) ISO file.
💡Tip: Before we can use this plugin, we need to install it explicitly using packer plugins install github.com/hashicorp/vmware
or if you using Packer v1.7+ and the template contains the required_plugins
section we can use packer init
to install all required plugins instead.
The common and well known keys have been covered in the previous section. Here I would like to focus on VMware Workstation specific keys, which include:
vmx_data
— defines a list of key-value pairs that are inserted in to the VM configuration file. This is useful if a builder does not support a configuration option directly.vmx_data_post
— similar to previous one, defines a list of key-value pairs that is inserted to VM configuration file before the export process.guest_os_type
— can be used to further optimize the virtual machine settings based on OS recommendations from VMware Workstation.vmx_remove_ethernet_interfaces
is useful when this virtual machine template will be used with Vagrant as it will create new interfaces during provisioning.
The following code snipped displays the entire configuration section for this particular builder:
source "vmware-iso" "efi" {
boot_command = [
"c",
"linux /casper/vmlinuz autoinstall net.ifnames=0 biosdevname=0 ",
"ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/' --- <enter><wait>",
"initrd /casper/initrd<enter><wait>",
"boot<enter>"
]
boot_wait = "5s"
communicator = "ssh"
vm_name = "packer-${var.name}"
cpus = "${var.cpus}"
memory = "${var.memory}"
disk_size = "${var.disk_size}"
iso_urls = "${var.iso_urls}"
iso_checksum = "${var.iso_checksum}"
headless = false
http_directory = "http"
ssh_username = "vagrant"
ssh_password = "vagrant"
ssh_port = 22
ssh_timeout = "3600s"
vnc_disable_password = true
vnc_bind_address = "127.0.0.1"
vmx_data = {
"firmware" = "efi"
}
vmx_data_post = {
"virtualHW.version": "18",
"cleanShutdown": "true",
"softPowerOff": "true",
"ethernet0.virtualDev": "e1000",
"ethernet0.startConnected": "true",
"ethernet0.wakeonpcktrcv": "false"
}
guest_os_type = "ubuntu-64"
vmx_remove_ethernet_interfaces = true
version = 18
tools_upload_flavor = "linux"
output_directory = "builds/${var.name}-${source.name}-${source.type}"
shutdown_command = "echo 'vagrant' | sudo -S shutdown -P now"
}
virtualbox-iso
The vmware-iso is required when we want to create a new virtual machine template for Oracle VirtualBox. The starting point for this builder is an Operating System (OS) ISO file.
The common and well known keys have been covered in the previous section. Here I would like to focus on VMware Workstation specific keys, which include:
firmware
— defines virtual machine firmware, BIOS and EUFI are supportedvboxmanage
— defines commands to execute in order to customize the virtual machine. This is useful if the builder does not support an option natively through a configuration key.guest_os_type
— similar as with VMware, when set correctly, it can optimize virtual machine performance based on best practices.
💡Tip: In order to build this VM template using VirtualBox 7.x it is required to modify the VM properties through vboxmanage
key. Otherwise, the VM will not be able to reach the HTTP server at 10.0.2.2 created by Packer.
source "virtualbox-iso" "efi" {
boot_command = [
"c",
"linux /casper/vmlinuz autoinstall net.ifnames=0 biosdevname=0 ",
"ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/' --- <enter><wait>",
"initrd /casper/initrd<enter><wait>",
"boot<enter>"
]
boot_wait = "5s"
communicator = "ssh"
vm_name = "packer-${var.name}"
cpus = "${var.cpus}"
memory = "${var.memory}"
disk_size = "${var.disk_size}"
iso_urls = "${var.iso_urls}"
iso_checksum = "${var.iso_checksum}"
headless = false
http_directory = "http"
ssh_username = "vagrant"
ssh_password = "vagrant"
ssh_port = 22
ssh_timeout = "3600s"
firmware = "efi"
vboxmanage = [
["modifyvm", "{{.Name}}", "--nat-localhostreachable1", "on"],
]
guest_os_type = "Ubuntu_64"
output_directory = "builds/${var.name}-${source.name}-${source.type}"
shutdown_command = "echo 'vagrant' | sudo -S shutdown -P now"
}
Builders, Provisioners and Post-Processors
The build section ties together list of sources along with provisioners and post-processors.
Provisioner subsection is the place where we define which shell scripts will be executed once the OS installation completes and the machine reboots.
Once the provisioning phase is completed and the VM is shut down the post-processors are called to action.
The vagrant
post-processor is used to generate and store Vagrant box on local filesystem.
The vagrant-cloud
post-processor is then used to upload the box it to specific account hosted at Vagrant Cloud.
💡Tip: In order to successfully export the box to Vagrant Cloud, you need to ensure that the environment variable VAGRANT_CLOUD_TOKEN
contains valid access token and you need ensure that the box tag already exists. It it doesn’t you can use the following API request to create one.
build {
sources = ["hyperv-iso.efi", "vmware-iso.efi", "virtualbox-iso.efi"]
provisioner "shell" {
environment_vars = ["HOME_DIR=/home/vagrant", "http_proxy=${var.http_proxy}", "https_proxy=${var.https_proxy}", "no_proxy=${var.no_proxy}"]
execute_command = "echo 'vagrant' | {{ .Vars }} sudo -S -E sh -eux '{{ .Path }}'"
scripts = ["scripts/setup.sh", "scripts/vagrant.sh", "scripts/cleanup.sh"]
expect_disconnect = true
}
post-processors {
post-processor "vagrant" {
output = "builds/${var.name}-{{.Provider}}.box"
}
post-processor "vagrant-cloud" {
box_tag = "maroskukan/${var.name}"
version = "${local.version}"
}
}
Workflow
The following workflow describes the process of building and publishing the template on Windows Build environment:
# Clone the remote repository
git clone https://github.com/maroskukan/packer-cookbook
cd packer-cookbook\boxes\ubuntu2304\
# Setup Access token
$ENV:VAGRANT_CLOUD_TOKEN = "your-vagrant-cloud-access-token"
# Install Required Plugins
packer init .\box-config.pkr.hcl
# Validate
packer validate .\box-config.pkr.hcl
# Build and push
packer build .\box-config.pkr.hcl
If you use Linux Build environment, the process would be as follows:
# Setup Access token
export VAGRANT_CLOUD_TOKEN = "your-vagrant-cloud-access-token"
# Install Required Plugins
packer init ./box-config.pkr.hcl
# Validate
packer validate ./box-config.pkr.hcl
# Build and push
packer build ./box-config.pkr.hcl
💡Tip: Depending on your build strategy, it might be viable to limit build process to specific source at a time using -only=
option. For example packer build -only="hyperv-iso.efi" .\box-config.pkr.hcl
.
The resulting Ubuntu 23.04 templates are available at Vagrant Cloud.
Closing thoughts
In summary, automating golden image builds with Packer revolutionizes infrastructure deployment processes, offering efficiency and consistency.
By harnessing Packer’s automation capabilities, we can streamline our image creation workflows, reduce errors and save valuable time. The possibilities are endless. I would love to hear your thoughts and experience with Packer and image generation.