Quick introduction[edit | edit source]

Packer automates the creation of VM images. To that end, you need to tell Packer where to build the VM, how to boot the VM, and how to finish setting up the VM. Each of these steps are defined in the builder and one or more provisioner components.

To help you get a sense of what Packer can do, this page will go over how to create a simple Alpine Linux template on Proxmox using Packer.

Defining the Packer configuration[edit | edit source]

You can either write a Packer configuration in native HCL (HashCorp's own language) or JSON. Native HCL configs should end with a .pkr.hcl file extension, otherwise it will be read as JSON. I'll be using the older json format for now.

We will define two things in our configuration: a builder, and any number of provisioners.

A builder is what Packer uses to interface with the hypervisor and can be something local (like VirtualBox or VMware Workstation), something remote (like a Proxmox server), or something in the cloud (like Azure, AWS EC2). You tell the builder what type of VM you want to build, what ISOs to attach, and what size of disk to use. In the example below, we use the Proxmox builder that's included with Packer. Review the builder's documentation to see what parameters are required. At the minimum, we need to specify our Proxmox server and authentication information, the network the VM should attach to, and a ISO boot media. The VM configuration can also be defined here.

Packer automates the OS installation step by sending keystrokes defined in the "boot_command" list. Everything defined in this list will be typed into the VM in sequence character by character. Some special keystrokes like the enter key or arrow keys are defined as special strings like <enter> and <up>. Short delays can be inserted with <wait> or <wait1>, and even longer delays are possible with <wait10m> for 10 minutes or <wait10h> for 10 hours. Review the builder's documentation for a detailed list of supported boot commands.

Once the OS installation is finished, Packer will then wait for the VM to come online using SSH in order to finish the provisioning process. For non-Linux based systems, some Packer builders also supports WinRM. If you have no provisioners defined, Packer will still need to connect to the VM via SSH in order to confirm the build was successful. Packer supports a wide array of configuration management tools such as Ansible, Salt, and Puppet as well as running plain old shell scripts as the provisioning step.

In the example below, I will set up Alpine Linux 3.15 on Proxmox from an ISO. I've added the appropriate boot_commands so that setup-alpine works properly (the answers file doesn't seem to work?). Once the setup is complete, I add the QEMU guest agent so that Packer is able to determine the guest's IP address and connect to it via SSH using password based authentication. Once Packer connects for the first time, it will run a series of scripts in the provisioning step to finalize the VM.

{
  "variables": {
    "username": "terraform@pam!terraform_token_id",
    "token": "***"
  },

  "builders": [
    {
      "type": "proxmox",
      "proxmox_url": "https://server.home.steamr.com:8006/api2/json",
      "insecure_skip_tls_verify": true,
      "username": "{{user `username`}}",
      "token": "{{user `token`}}",
      "node": "server",

      "network_adapters": [{"bridge": "vmbr0"}],

      "scsi_controller": "virtio-scsi-single",
      "disks": [
        {
          "type": "scsi",
          "disk_size": "5G",
          "storage_pool": "data",
          "storage_pool_type": "zfspool"
        }
      ],
	  
      "iso_url": "http://private-mirror/alpine-virt-3.15.0-x86_64.iso",
      "iso_checksum": "e97eaedb3bff39a081d1d7e67629d5c0e8fb39677d6a9dd1eaf2752e39061e02",
      "iso_storage_pool": "local",
	  
      "http_directory": "http",
      "boot_wait": "12s",
      "boot_command": [
        "root<enter><wait>",
        "ifconfig eth0 up \u0026\u0026 udhcpc -i eth0<enter><wait10>",
        "wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/answers&lt;enter&gt;&lt;wait&gt;",
        "BOOT_SIZE=50 setup-alpine -f answers<enter><wait2>",
        "us us<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "{{user `root_pw`}}&lt;enter&gt;&lt;wait&gt;",
        "{{user `root_pw`}}&lt;enter&gt;&lt;wait3&gt;",
        "America/Edmonton<enter><wait1>",
        "<enter><wait1>",
        "<enter><wait1>",
        "openssh<enter><wait1>",
        "sda<enter><wait1>",
        "lvm<enter><wait1>",
        "sys<enter><wait1>",
        "y<enter><wait20>",
        "rc-service sshd stop<enter>",
        "mount /dev/vg0/lv_root /mnt<enter>",
        "mount --bind /dev /mnt/dev<enter>",
        "chroot /mnt<enter>",
        "echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config<enter>",
        "echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config<enter><wait1>",
        "sed -i -e 's/#//g' /etc/apk/repositories<enter>",
        "apk update<enter><wait5>",
        "apk add --no-cache qemu-guest-agent<enter><wait5>",
        "rc-update add local default<enter>",
        "echo -e '#!/bin/sh\\nqemu-ga -d -p /dev/vport1p1' > /etc/local.d/Qemu.start<enter>",
        "chmod 755 /etc/local.d/Qemu.start<enter>",
        "exit<enter>",
        "umount /mnt/dev<enter><wait1>",
        "umount /mnt<enter><wait1>",
        "reboot<enter>"
      ],
	  
      "ssh_username": "root",
      "ssh_password": "{{user `root_pw`}}",
      "ssh_timeout": "15m",

      "unmount_iso": true,
      "template_name": "alpine",
      "template_description": "Alpine Linux, generated on {{ isotime \"2006-01-02T15:04:05Z\" }}"
    }
  ],
  
  "provisioners": [
    {
      "scripts": [
        "scripts/00_base.sh",
        "scripts/01_cloudinit.sh",
        "scripts/02_docker.sh",
        "scripts/05_misc.sh",
        "scripts/99_cleanup.sh"
      ],
      "type": "shell"
    }
  ]
}