Using Template Files with HashiCorp Packer

Learn about the new HCL2-only templatefile function and how to use it in an http_content to preseed a virtual machine.

In HashiCorp Packer 1.7, we tagged HCL2 as stable and implemented HCL2-only functions. You can use one such function, the templatefile function to build multiple operating systems with less duplication of configuration.

Currently, you need to use the boot_command argument to configure an OS before you connect to the machine. You can use it with many builders, including the vmware-iso or virtualbox-iso builders. A boot_command mimicks manual keystrokes and sends them at a regular cadence. You aggregate these keystrokes for installing and configuring packages in a preseed file. Packer enables sharing preseed files by making them available statically through an HTTP server. You can also access static files using CD files or a floppy.

In this post, we’ll use the http_content and the templatefile functions together to build preseed file templates for two Ubuntu images, one with HashiCorp Nomad and one with HashiCorp Consul.

»Templating a Preseed File

Say the file preseed.pkrtpl is your preseed template file, and you would like to be able to set a user’s name, ID, and password and also the packages installed with it :

d-i apt-setup/universe boolean true
d-i pkgsel/include %{ for install in installs ~}${install} %{ endfor }string
 openssh-server cryptsetup build-essential libssl-dev libreadline-dev zlib1g-dev
 linux-source dkms nfs-common linux-headers-$(uname -r) perl cifs-utils
 software-properties-common rsync ifupdown

d-i passwd/user-fullname string ${}
d-i passwd/user-uid string ${}
d-i passwd/user-password password ${user.password}
d-i passwd/user-password-again password ${user.password}
d-i passwd/username string ${}

choose-mirror-bin mirror/http/proxy string
d-i base-installer/kernel/override-image string linux-server
d-i clock-setup/utc boolean true
d-i clock-setup/utc-auto boolean true
d-i finish-install/reboot_in_progress note
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i mirror/country string manual
d-i mirror/http/directory string /ubuntu/
d-i mirror/http/hostname string
d-i mirror/http/proxy string
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman-auto/method string lvm
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-lvm/device_remove_lvm boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_write_new_label boolean true
d-i pkgsel/install-language-support boolean false
d-i pkgsel/update-policy select none
d-i pkgsel/upgrade select full-upgrade
d-i time/zone string UTC
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false
tasksel tasksel/first multiselect standard, server

This template file can be used to install binaries from a variable. In this example, we made it possible to configure the user settings and pass an arbitrary list of packages to install. This simplifies the build while making it more powerful.

Note: The .pkrtpl extension is a recommendation and not a requirement. It allows editors to recognize a Packer template file written in HCL2.

The following configuration file can then be defined:

variables {
  headless = true

source "virtualbox-iso" "base-ubuntu-amd64" {
  headless                = var.headless

  iso_url                 = local.ubuntu_2010_iso_url
  iso_checksum            = "file:${local.ubuntu_2010_iso_checksum_url}"

  guest_os_type           = "Ubuntu_64"
  hard_drive_interface    = "sata"
  ssh_wait_timeout        = "15m"
  boot_wait               = "5s"

locals {
  ubuntu_2010_dl_folder        = ""
  ubuntu_2010_iso_url          = "${local.ubuntu_2010_dl_folder}ubuntu-18.04.5-server-amd64.iso"
  ubuntu_2010_iso_checksum_url = "${local.ubuntu_2010_dl_folder}SHA256SUMS"

  builds = {
    consul = {
      user = {
        id       = 1000
        name     = "bob"
        password = "s3cr2t"
      installs = ["consul"]
    nomad = {
      user = {
        id       = 1000
        name     = "bob"
        password = "s3cr2t"
      installs = ["nomad"]

build {
  name = "ubuntu"
  description = <<EOF
This build creates images for :
* Ubuntu 18.04
For the following builders :
* virtualbox-iso
It will create base images with:
* Nomad
* Consul

  dynamic "source" {
    for_each = local.builds

    labels   = ["source.virtualbox-iso.base-ubuntu-amd64"]
    content {
      name                    = source.key

      ssh_username            =
      ssh_password            = source.value.user.password
      shutdown_command        = "echo '${source.value.user.password}' | sudo -S shutdown -P now"

      http_content            = {
        "/preseed.cfg" = templatefile("${path.root}/preseed.pkrtpl", source.value)

      boot_command     = [
        " auto<wait>",
        " console-setup/ask_detect=false<wait>",
        " console-setup/layoutcode=us<wait>",
        " console-setup/modelcode=pc105<wait>",
        " debconf/frontend=noninteractive<wait>",
        " debian-installer=en_US.UTF-8<wait>",
        " fb=false<wait>",
        " initrd=/install/initrd.gz<wait>",
        " kbd-chooser/method=us<wait>",
        " keyboard-configuration/layout=USA<wait>",
        " keyboard-configuration/variant=USA<wait>",
        " locale=en_US.UTF-8<wait>",
        " netcfg/get_domain=vm<wait>",
        " netcfg/get_hostname=vagrant<wait>",
        " grub-installer/bootdev=/dev/sda<wait>",
        " noapic<wait>",
        " preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg<wait>",
        " -- <wait>",
      output_directory = "virtualbox_iso_ubuntu_2010_amd64_${source.key}"

  provisioner "shell" {
    environment_vars  = [ "HOME_DIR=/home/vagrant" ]
    execute_command   = "echo '${local.builds[].user.password}' | {{.Vars}} sudo -S -E sh -eux '{{.Path}}'"
    expect_disconnect = true
    inline            = [
      "echo hello from the ${} image",
      "${} version"

Now we can see that this has made it easier to maintain a preseed project. Pretty cool, right? Effectively, the templatefile function can be used in other scenarios, same for the http_content option. We could extend this by skipping the template file and setting everything from the map:

http_content = {
  "/preseed.cfg" = <<<EOF
d-i apt-setup/universe boolean true
d-i pkgsel/include string openssh-server cryptsetup build-essential libssl-dev libreadline-dev zlib1g-dev linux-source dkms nfs-common linux-headers-$(uname -r) perl cifs-utils software-properties-common rsync ifupdown consul


For more information on Packer’s recent additions, review Packer's changelog. Lastly, if you have any issues, do not hesitate to open an issue in the Packer repository on GitHub.

Sign up for the latest HashiCorp news