Skip to main content

HashiCorp Packer Machine Images Introduction

TST, Hong Kong

HashiCorp Packer automates the creation of any type of machine image. It embraces modern configuration management by encouraging you to use automated scripts to install and configure the software within your Packer-made images. Packer brings machine images into the modern age, unlocking untapped potential and opening new opportunities.

Installing Packer

I am going to install Packer manually on an Arch LINUX Desktop using the pre-compiled binary. Download the ZIP file to your Download directory and unzip it.

Print a colon-separated list of locations in your PATH:

echo $PATH

Move the Packer binary to one of the listed locations. This command assumes that the binary is currently in your downloads folder and that your PATH includes /usr/local/bin, but you can customize it if your locations are different:

mv ~/Downloads/packer /usr/local/bin/

After installing Packer, verify the installation worked by opening a new command prompt or console, and checking that packer is available:

packer
usage: packer [--version] [--help] <command> [<args>]

Available commands are:
    build       build image(s) from template
    fix         fixes templates from old versions of packer
    inspect     see components of a template
    validate    check that a template is valid
    version     Prints the Packer version

Configuring ZSH (Optional)

To install Packer autocompletion in oh-my-zsh clone this repo:

git clone https://github.com/gunzy83/packer-zsh-completion.git ~/.oh-my-zsh/plugins/packer

Then add the plugin to your plugin list in oh-my-zsh configuration:

nano ~/.zshrc

plugins=(... packer)

After installation re-source your .zshrc:

source .zshrc

Installing Packer Plugins

Installing Golang

Packer as well as it's plugins are written in Go. To install a plugin from source make sure that you have go & go-tools installed:

sudo pacman -S go

go version
go version go1.15.2 linux/amd64

You can check that Go is installed correctly by building a simple program hello.go, as follows:

package main

import "fmt"

func main() {
    fmt.Println("Hello, Terminal!")
}

Then run it with the go tool:

go run ./hello.go

Hello, Terminal!

Building a Packer Plugin from Source

Start by cloning the repository that you want to use and enter the directory, e.g. :

git clone https://github.com/SwampDragons/packer-provisioner-comment.git
cd ./packer-provisioner-comment

Now install all Go dependencies the application needs and run the build command to create the binary that we can install in Packer:

go mod download
go build

This will output a binary file called main inside the directory. We can rename it and move it either into /usr/local/bin/ to have it available globally. But to keep our system clean we can also copy it into our users home directory:

mkdir -p ~/.packer.d/plugins
mv main ~/.packer.d/plugins/packer-provisioner-comment

Packer Builders

In the builders section we need to define the platform that want to deploy to and add the necessary configuration like API keys and source images.

Docker Builder

The Docker Packer builder builds Docker images using Docker. The builder starts a Docker container, runs provisioners within this container, then exports the container for reuse or commits the image. Packer builds Docker containers without the use of Dockerfiles. The Docker builder must run on a machine that has Docker Engine installed.

Basic Example: Export

Below is a fully functioning example. It doesn't do anything useful, since no provisioners are defined, but it will effectively repackage an image.

{
    "builders": [
      {
        "export_path": "ubuntu.tar",
        "image": "ubuntu",
        "type": "docker"
      }
    ]
  }

Validating and Running your Template File

packer validate ./packer.json
packer fix ./packer.json
packer build packer.json

docker: output will be in this color.
==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
    docker: Using default tag: latest
    docker: latest: Pulling from library/ubuntu
    docker: Digest: sha256:fff16eea1a8ae92867721d90c59a75652ea66d29c05294e6e2f898704bdb8cf1
    docker: Status: Image is up to date for ubuntu:latest
    docker: docker.io/library/ubuntu:latest
==> docker: Starting docker container...
    docker: Run command: docker run -v /home/user/.packer.d/tmp926764499:/packer-files -d -i -t --entrypoint=/bin/sh -- ubuntu
    docker: Container ID: adcc271a70e326726d1fdf48e256e437c0abc1644749a86a87c4550860319ae9
==> docker: Using docker communicator to connect: 172.17.0.2
==> docker: Exporting the container
==> docker: Killing the container: adcc271a70e326726d1fdf48e256e437c0abc1644749a86a87c4550860319ae9
Build 'docker' finished after 6 seconds 890 milliseconds.
==> Wait completed after 6 seconds 890 milliseconds
==> Builds finished. The artifacts of successful builds are:
--> docker: Exported Docker file: ubuntu.tar

Basic Example: Commit

Below is another example, the same as above but instead of exporting the running container, this one commits the container to an image. The image can then be more easily tagged, pushed, etc.

{
  "builders": [
    {
      "commit": true,
      "image": "ubuntu",
      "type": "docker"
    }
  ]
}

Basic Example: Changes to Metadata

Below is an example using the changes argument of the builder. This feature allows the source images metadata to be changed when committed back into the Docker environment. It is derived from the docker commit --change command line option to Docker.

Example uses of all of the options, assuming one is building an NGINX image from ubuntu as an simple example:

{
  "builders": [
    {
      "changes": [
        "USER www-data",
        "WORKDIR /var/www",
        "ENV HOSTNAME www.example.com",
        "VOLUME /test1 /test2",
        "EXPOSE 80 443",
        "LABEL version=1.0",
        "ONBUILD RUN date",
        "CMD [\"nginx\", \"-g\", \"daemon off;\"]",
        "ENTRYPOINT /var/www/start.sh"
      ],
      "commit": true,
      "image": "ubuntu",
      "type": "docker"
    }
  ]
}

Building and Committing your Image

packer build packer.json 

docker: output will be in this color.
==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
    docker: Using default tag: latest
    docker: latest: Pulling from library/ubuntu
    docker: Digest: sha256:fff16eea1a8ae92867721d90c59a75652ea66d29c05294e6e2f898704bdb8cf1
    docker: Status: Image is up to date for ubuntu:latest
    docker: docker.io/library/ubuntu:latest
==> docker: Starting docker container...
    docker: Run command: docker run -v /home/user/.packer.d/tmp556583891:/packer-files -d -i -t --entrypoint=/bin/sh -- ubuntu
    docker: Container ID: c5126f38aa32ff58fe3ce11622be59a9f6216c4478d93e34719f89e1c522f2ac
==> docker: Using docker communicator to connect: 172.17.0.2
==> docker: Committing the container
    docker: Image ID: sha256:e186c58d2da00ec11c2b273941b6c61142fcdaae3d2949e9135278fc36615713
==> docker: Killing the container: c5126f38aa32ff58fe3ce11622be59a9f6216c4478d93e34719f89e1c522f2ac
Build 'docker' finished after 11 seconds 495 milliseconds.
==> Wait completed after 11 seconds 495 milliseconds
==> Builds finished. The artifacts of successful builds are:
--> docker: Imported Docker image: sha256:e186c58d2da00ec11c2b273941b6c61142fcdaae3d2949e9135278fc36615713
docker images
REPOSITORYTAGIMAGE IDCREATEDSIZE
nonenonee186c58d2da0About a minute ago72.9MB

Packer Communicator

By default Packer uses SSH to create the machine from your template. If you need to use another communicator, e.g. WinRM on Windows images, those have to be defined.

The communicator needs to be configured with the default user login to the source image that you want to use, e.g. :

{
  ...
  "ssh_username": "ubuntu",
  "ssh_password": "ubuntu"
}