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 trued-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 stringd-i base-installer/kernel/override-image string linux-serverd-i clock-setup/utc boolean trued-i clock-setup/utc-auto boolean trued-i finish-install/reboot_in_progress noted-i grub-installer/only_debian boolean trued-i grub-installer/with_other_os boolean trued-i mirror/country string manuald-i mirror/http/directory string /ubuntu/d-i mirror/http/hostname string archive.ubuntu.comd-i mirror/http/proxy stringd-i partman-auto-lvm/guided_size string maxd-i partman-auto/choose_recipe select atomicd-i partman-auto/method string lvmd-i partman-lvm/confirm boolean trued-i partman-lvm/confirm boolean trued-i partman-lvm/confirm_nooverwrite boolean trued-i partman-lvm/device_remove_lvm boolean trued-i partman/choose_partition select finishd-i partman/confirm boolean trued-i partman/confirm_nooverwrite boolean trued-i partman/confirm_write_new_label boolean trued-i pkgsel/install-language-support boolean falsed-i pkgsel/update-policy select noned-i pkgsel/upgrade select full-upgraded-i time/zone string UTCd-i user-setup/allow-password-weak boolean trued-i user-setup/encrypt-home boolean falsetasksel 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

By submitting this form, you acknowledge and agree that HashiCorp will process your personal information in accordance with the Privacy Policy.