A custom Vagrant box with Packer
Tags: tutorial, linux
Published: 2021-03-13 22:30:58

[Packer](https://packer.io/) is an open source tool developed by [Hashicorp](https://hashicorp.com) for automating the creation of any type of machine image. You can use it for creating a custom Vagrant box for your development environment. That's what I will cover on this article.

## Installation
You can get the installer for your operating system by going to [packer.io/downloads](https://packer.io/downloads). If you're on Linux you can get it from the repositories of some distributions. 

Check the [Install Packer](https://learn.hashicorp.com/tutorials/packer/getting-started-install) section for installation instructions of some Linux distros including Ubuntu/Debian, Centos/RHEL, Fedora and Amazon Linux.

Note: If you get the following error on Debian when trying to add the Hashicorp repository, just install the package `software-properties-common`.

```bash
sudo: apt-add-repository: command not found
```

```bash
$ sudo apt-get install -y software-properties-common
```

For Arch-based distributions, you can install Packer from the official repositories. Not to be confused with the `packer` package available from AUR, this is an old package manager for Arch.

## Vagrant boxes
Packer uses JSON and HCL templates for knowing the instructions to execute for building images. HCL is the format recommended for Packer 1.7 or later but still has support for JSON templates.

You must create an HCL or JSON file following the naming convention recommended by Packer, `file_name.pkr.json` or `file_name.pkr.hcl`.

Don't forget to create a directory for storing the configuration files for Packer and Vagrant.

```bash
$ mkdir virtual-environment
$ cd virtual-environment
```

### JSON
Create a JSON file with the following content:
```json
{
  "provisioners": [
    {
      "type": "shell",
      "execute_command": "echo 'vagrant' | {{.Vars}} sudo -S -E bash '{{.Path}}'",
      "script": "scripts/setup.sh"
    }
  ],
  "builders": [
    {
      "communicator": "ssh",
      "source_path": "ubuntu/focal64",
      "provider": "virtualbox",
      "add_force": true,
      "type": "vagrant"
    }
  ]
}
```

In the `builders` section:
- `communicator` is set to `ssh` as Vagrant will access the virtual environment through an SSH connection.

- `ubuntu/focal64`, assigned to `source_path`, is the base image that Packer will use during the building process.

- The provider that Vagrant will use is VirtualBox.

- `add_force` set to true is the same as using the `--force` flag when running any `vagrant box` instruction.

- The builder type is set to `vagrant` for building a custom Vagrant box.

In the `provisioners` section:
Vagrant has support for shell scripts, Ansible, Chef or Puppet as provisioners.

If you're using a shell script for provisioning your environment you have to define the following variables in this section.

- `type` set to `shell` for using shell scripts.

- `execute_command` is the instruction that Packer will run for provisioning the environment.

- The path of the script is set in the `script` variable.

### HCL
If you're still using JSON templates, you can use the `hcl2_upgrade` command for converting from JSON to HCL.

If you want to convert the file from the previous example, just run:

```bash
packer hcl2_upgrade -output-file=file_name.pkr.hcl file_name.pkr.json
```

The HCL template generated after running the above command will have the following content:

```hcl
variable "version" {
  type    = string
  default = ""
}

locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }

source "vagrant" "autogenerated_1" {
  add_force    = true
  communicator = "ssh"
  provider     = "virtualbox"
  source_path  = "ubuntu/focal64"
}

build {
  sources = ["source.vagrant.autogenerated_1"]

  provisioner "shell" {
    execute_command = "echo 'vagrant' | {{.Vars}} sudo -S -E bash '{{.Path}}'"
    script = "scripts/setup.sh"
  }
}
```

### Provisioning
Create a directory named `scripts`.

```bash
$ mkdir scripts
```

Inside the `scripts` directory, create a shell script named `setup.sh` that contains the instructions required for configuring the tools you need for your virtual environment.

You can use other provisioning tools like Ansible, Chef or Puppet but this time you will use a bash script.

For example, if you want to configure an environment for Python, your script will have the following content:

```bash
apt-get update
apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev python-openssl git

su - vagrant -c "curl https://pyenv.run | bash

echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3"
```

Instructions in your shell script will be executed by the `root` user, that's why you don't need to run `apt-get update` or `apt-get install` with `sudo`.

For configuring tools like Poetry and pyenv you need to run installation and configuration instructions with the `vagrant` user, as this is the user you will log in with. In that case, run those instructions with `su - vagrant -c`.

### Building
Run `packer build` after creating the template for Packer:

```bash
$ packer build file_name.pkr.json
```

```bash
$ packer build file_name.pkr.hcl
```

The above command will create a directory named `output-vagrant` where you can find a `package.box` file and a `Vagrantfile`.

You need the `package.box` file for configuring your virtual environment with Vagrant.

## Configuring Vagrant
After creating the custom Vagrant box, you have to configure the virtual environment with Vagrant.

Create a Vagrantfile:
```bash
$ vagrant init dev-env
```

Where `dev-env` is the value assigned to the `config.vm.box` variable in the Vagrantfile. You can replace it according to your needs.

Add the `package.box` file to Vagrant:
```bash
$ vagrant box add dev-env output-vagrant/package.box
```

Now your virtual environment is ready. You can initialize it and log in through SSH.

```bash
$ vagrant up
$ vagrant ssh
```

You're ready to go!