Introduction

Hashicorp’s Packer is an image automation tool. I will briefly discuss the configuration of an image template to create a snapshot (image) of Ubuntu 20.04 LTS on DigitalOcean. I will quickly add that this article will be a part of a series of Hashicorp related articles. I will, in future, present/write on Nomad, Terraform, etc. All fantastic products from what appears to be a solid company!

Anyhow, to briefly explain what I want to do here with Packer, I simply want to create a snapshot that contains various Hashicorp products as well as a docker and docker-compose. I will eventually use this snapshot to provision a set of clients and servers to create a nomad cluster on DigitalOcean.

Configuration

To begin, assuming you have Packer installed, we need to create several json files.

The first file, which is json based, will contain our variables. You will need to create a DigitalOcean API Key. Afterwards, select the base system image, this case ubuntu-20-04-x64, region, tor1, and size, s-1vcpu-1gb, of the droplet. Those details can be found from the DigitalOcean API by calling various endpoints.

{
  "do_token": "<apikey>",
  "base_system_image": "ubuntu-20-04-x64",
  "region": "tor1",
  "size": "s-1vcpu-1gb" <-- droplet size
}

Now we can begin to work with the template for our snapshot. The following template, which I use myself, contains a builder and various provisioners. But wait, hang on, what are builders and provisioners? So actually let’s step back for a minute. Templates CAN contain several keys, namely builders, provisioners, post-processors, variables and a description. However, only the builders key is mandatory.

By definition, from Packer, the builders key is an array of one or more objects that defines the builders that will be used to create machine images for this template, and configures each of those builders. There are separate builders for EC2, VMware, VirtualBox, etc, but in my case I am using the digitalocean builder.

The provisioners key is an array of one or more objects that defines the provisioners that will be used to install and configure software for the machines created by each of the builders. Like builders, there are a number of provisioners that can/could be used. For more information, review the documentation on the matter. In the code below I use only shell and file provisioners.

{
  "builders": [
    {
      "type": "digitalocean",
      "api_token": "{{user `do_token`}}",
      "image": "{{user `base_system_image`}}",
      "region": "{{user `region`}}",
      "size": "{{user `size`}}",
      "ssh_username": "root"
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sleep 30",
        "sudo apt-get clean",
        "sudo apt-get update",
        "sudo apt-get install -y apt-transport-https ca-certificates nfs-common",
        "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -",
        "curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -",
        "sudo add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"",
        "sudo apt-add-repository \"deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main\"",
        "sudo apt-get update",
        "sudo apt-get install -y docker-ce nomad consul vault ufw unzip",
        "sudo curl -L \"https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose",
        "sudo chmod +x /usr/local/bin/docker-compose",
        "wget https://releases.hashicorp.com/consul-template/0.25.1/consul-template_0.25.1_linux_amd64.zip",
        "unzip consul-template_0.25.1_linux_amd64.zip",
        "cp consul-template /usr/local/bin/consul-template",
        "sudo chmod +x /usr/local/bin/consul-template",
        "sudo mkdir -p /root/nomad/jobs"
      ]
    },
    {
      "type": "file",
      "source": "consul/configure_consul.sh",
      "destination": "/root/configure_consul.sh"
    },
    {
      "type": "file",
      "source": "consul/consul-server.service",
      "destination": "/etc/systemd/system/consul-server.service"
    },
    {
      "type": "file",
      "source": "consul/consul-connect-enable.hcl",
      "destination": "/root/consul-connect-enable.hcl"
    },
    {
      "type": "file",
      "source": "consul/consul-client.service",
      "destination": "/etc/systemd/system/consul-client.service"
    },
    {
      "type": "file",
      "source": "nomad/nomad-server.hcl",
      "destination": "/root/nomad-server.hcl"
    },
    {
      "type": "file",
      "source": "nomad/nomad-client.hcl",
      "destination": "/root/nomad-client.hcl"
    },
    {
      "type": "file",
      "source": "nomad/configure_nomad.sh",
      "destination": "/root/configure_nomad.sh"
    },
    {
      "type": "file",
      "source": "nomad/nomad-client.service",
      "destination": "/etc/systemd/system/nomad-client.service"
    },
    {
      "type": "file",
      "source": "nomad/nomad-server.service",
      "destination": "/etc/systemd/system/nomad-server.service"
    },
    {
      "type": "file",
      "source": "nomad/jobs/traefik.nomad",
      "destination": "/root/nomad/jobs/traefik.nomad"
    },
    {
      "type": "file",
      "source": "nomad/jobs/jessequinn.nomad",
      "destination": "/root/nomad/jobs/jessequinn.nomad"
    },
    {
      "type": "file",
      "source": "nomad/jobs/scidoc.nomad",
      "destination": "/root/nomad/jobs/scidoc.nomad"
    },
    {
      "type": "file",
      "source": "nomad/jobs/fabio.nomad",
      "destination": "/root/nomad/jobs/fabio.nomad"
    },
    {
      "type": "file",
      "source": "vault/vault-config.hcl",
      "destination": "/root/vault-config.hcl"
    },
    {
      "type": "file",
      "source": "vault/vault-server.service",
      "destination": "/etc/systemd/system/vault-server.service"
    },
    {
      "type": "file",
      "source": "vault/enable_vault.sh",
      "destination": "/root/enable_vault.sh"
    },
    {
      "type": "file",
      "source": "vault/init_vault.sh",
      "destination": "/root/init_vault.sh"
    }
  ]
}

So to quickly explain what happened in the above code, I only used a single builder, digitalocean, where I passed several important variables to it. At which point, I configured a single shell provisioner to install docker, docker-compose, nomad, vault and consul. Afterwards, I use the file provisioner to copy specific local files to the snapshot. In this case, I copied all my scripts related to nomad, vault, consul, etc so that I can eventually use Terraform to provision a Nomad cluster.

To make life easier, I am a big fan of Makefiles.

.PHONY: validate
validate:
	@packer validate -var-file=variables.json template.json

.PHONY: build
build:
	@packer build -var-file=variables.json template.json

In the end, to build the snapshot we need to run the following command packer build -var-file=variables.json template.json. In the Makefile I also included a validate target.

One note, I showed all the code in json; however, Packer also supports hcl.

Final Words

Packer is a simple and easy to use image automation tool. It is not required when making provisions with Terraform, BUT, I found it to help organize/reduce the code in Terraform.