I've been using proxmox for a while now in my homelab as an open-source alternative to something like ESXi for a virtualization platform. With proxmox when can create templates for our VMS so we can have a standard starting point to install our applications on top of, these templates can be useful too so that you can pre-install packages for authentication, security, logging and etc without anyone else needing to think about it.

However, creating and managing these templates can become a challenge with how time-consuming and manual it can be. I want to show you how you can make this process more standardized and automated with the use of packer to allow you to declare your proxmox templates as code.

What is packer

Packer is a utility that allows you to build virtual machine images so that you can define a golden image as code. Packer can be used to create images for almost all of the big cloud providers such as AWS, GCE, Azure and Digital Ocean, or can be used with locally installed hypervisors such as VMWare, Proxmox and a few others.

To build an image with packer we need to define our image through a template file. The file uses the JSON format and comprises of 3 main sections that are used to define and prepare your image.

Builders: Components of Packer that are able to create a machine image for a single platform. A builder is invoked as part of a build in order to create the actual resulting images.

Provisioners: Install and configure software within a running machine prior to that machine being turned into a static image. Example provisioners include shell scripts, Chef, Puppet, etc.

Post Processors: Take the result of a builder or another post-processor and process that to create a new artifact. Examples of post-processors are compress and upload to compress and upload artifacts respectively, etc.

By using packer we can define our golden VM image as code so that we can easily build identically configured images on demand so that all your machines are running the same image and can also be easily updated to a new image when needed.

Preparing your packer template

For the following examples ill be referencing files from this example repo you can follow along with and should be a solid template to start off from for your own custom proxmox templates.

To create the template we will use the proxmox builder which connects through the proxmox web API to provision and configure the VM for us and then turn it into a template. To configure our template we will use a variables file, to import this variables file we will use the -var-file flag to pass in our variables to packer. These variables will be used in our template file with the following syntax within a string like so "passwd/username={{ user 'ssh_username'}}" .

Now the builder block below will outline the basic properties of our desired proxmox template such as its name, the allocated resources and the devices attached to the VM. To achieve this the boot_command option will be used to boot the OS and tell it to look for the preseed file to automate the OS installation process. Packer has an inbuilt HTTP server to serve this preseed.cfg file to the VM as its installing by using the http_directory option in the builder to specify the public files of the HTTP server. Check out the ubuntu preseed documentation for info on modifying the automatic installation process of the OS via a pre seed file.

{ "builders": [ { "type": "proxmox", "proxmox_url": "https://{{user `proxmox_host`}}:8006/api2/json", "insecure_skip_tls_verify": true, "username": "{{user `proxmox_api_user`}}", "password": "{{user `proxmox_api_password`}}", "vm_id": "{{ user `vmid` }}", "vm_name": "{{user `template_name`}}", "template_description": "{{ user `template_description` }}", "node": "{{user `proxmox_node`}}", "cores": "{{ user `cores` }}", "sockets": "{{ user `sockets` }}", "memory": "{{ user `memory` }}", "os": "l26", "network_adapters": [ { "model": "virtio", "bridge": "vmbr0" } ], "disks": [ { "type": "scsi", "disk_size": "{{ user `disk_size`}}", "storage_pool": "{{user `datastore`}}", "storage_pool_type": "{{user `datastore_type`}}", "format": "raw", "cache_mode": "writeback" } ], "ssh_timeout": "90m", "ssh_password": "{{ user `ssh_password` }}", "ssh_username": "{{ user `ssh_username` }}", "qemu_agent": true, "unmount_iso": true, "iso_file": "{{user `iso`}}", "http_directory": "./http", "boot_wait": "10s", "boot_command": [ "{{ user `boot_command_prefix` }}", "/install/vmlinuz ", "auto ", "console-setup/ask_detect=false ", "debconf/frontend=noninteractive ", "debian-installer={{ user `locale` }} ", "hostname={{ user `hostname` }} ", "fb=false ", "grub-installer/bootdev=/dev/sda<wait> ", "initrd=/install/initrd.gz ", "kbd-chooser/method=us ", "keyboard-configuration/modelcode=SKIP ", "locale={{ user `locale` }} ", "noapic ", "passwd/username={{ user `ssh_username` }} ", "passwd/user-fullname={{ user `ssh_fullname` }} ", "passwd/user-password={{ user `ssh_password` }} ", "passwd/user-password-again={{ user `ssh_password` }} ", "preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{ user `preseed.cfg` }} ", "-- <enter>" ] } ] }

In this template, we will also be using the shell provisioner to configure our VM os once it has been installed onto the virtual machine and is available via SSH. This can be helpful for installing the minimum required packages on your VM's such as the QEMU quest agent and cloud init or any other software required. You can also switch this provisioner auto for any of the other provisioners such as ansible, chef or puppet for example.

{ "provisioners": [ { "pause_before": "20s", "type": "shell", "environment_vars": ["DEBIAN_FRONTEND=noninteractive"], "inline": [ "date > provision.txt", "sudo apt-get update", "sudo apt-get -y upgrade", "sudo apt-get -y dist-upgrade", "sudo apt-get -y install linux-generic linux-headers-generic linux-image-generic", "sudo apt-get -y install qemu-guest-agent cloud-init", "sudo apt-get -y install wget curl", "exit 0" ] } ] }

And finally, we will use the post processors to run some commands locally. This will make an SSH connection to the PVE host and run some commands manually to set up the virtual devices necessary for cloud init. This post-processor is using the shell-local post processor to run the commands on the local machine running packer but you could always move this configuration to something like an ansible playbook to make the configuration more readable and portable.

Now with your configuration complete, you will be ready to build your proxmox template with packer. Run the command packer build -var-file="./config.json" ./ubuntu-18.04.json You should see some output for each of the builders , provisioners and post-processors .

$ packer build -var-file="./config.json" ./ubuntu-18.04.json proxmox: output will be in this color. ==> proxmox: Creating VM ==> proxmox: Starting VM ==> proxmox: Starting HTTP server on port 8771 ... Build 'proxmox' finished. ==> Builds finished. The artifacts of successful builds are: --> proxmox: A template was created: 4444 --> proxmox:

When the process is complete you should see your template ready in the proxmox interface and ready to be cloned into virtual machines.

Further reading on packer

You should now have a good starting point for building proxmox templates with packer. If your looking to extend its usefulness a little further check out these useful articles.

Cover photo by chuttersnap on Unsplash