Packer
Provision
In the previous tutorial, you created your first container image with Packer. However, the image you built essentially repackaged an existing Docker image. The real utility of Packer comes from automated provisioning in order to install and configure software in the machines prior to turning them into images.
Historically, pre-baked images have been frowned upon because changing them was so tedious and slow. Because Packer is completely automated (including provisioning) images can be changed quickly and integrated with modern configuration management tools such as Chef or Puppet.
In this tutorial, you will use provisioners to set an environment variable and create a file in your Docker image. Although defining environment variables and creating a file in a Docker image is a small example, it should give you an idea of what Packer provisioners can do.
Prerequisites
This tutorial assumes that you are continuing from the previous tutorials. If not, follow the steps below before continuing.
Install Packer
Create a directory named
packer_tutorial
and paste the following configuration into a file nameddocker-ubuntu.pkr.hcl
.packer { required_plugins { docker = { version = ">= 1.0.8" source = "github.com/hashicorp/docker" } } } source "docker" "ubuntu" { image = "ubuntu:jammy" commit = true } build { name = "learn-packer" sources = [ "source.docker.ubuntu" ] }
Initialize the Packer template.
$ packer init .
Once you have successfully initialized the template, you can continue with the rest of this tutorial.
Add provisioner to template
Using provisioners allows you to completely automate modifications to your image. You can use shell scripts, file uploads, and integrations with modern configuration management tools such as Chef or Puppet.
To write your first provisioner, add the following block into your Packer template, inside the build block and underneath the sources
assignment.
provisioner "shell" {
environment_vars = [
"FOO=hello world",
]
inline = [
"echo Adding file to Docker Container",
"echo \"FOO is $FOO\" > example.txt",
]
}
This block defines a shell
provisioner which sets an environment variable named FOO
in the shell execution environment and runs the commands in the inline
attribute. This provisioner will create a file named example.txt
that contains FOO is hello world
.
Your build block should look like the following.
build {
name = "learn-packer"
sources = [
"source.docker.ubuntu"
]
provisioner "shell" {
environment_vars = [
"FOO=hello world",
]
inline = [
"echo Adding file to Docker Container",
"echo \"FOO is $FOO\" > example.txt",
]
}
}
Build image
Build the image with the provisioner.
$ packer build docker-ubuntu.pkr.hcl
learn-packer.docker.ubuntu: output will be in this color.
==> learn-packer.docker.ubuntu: Creating a temporary directory for sharing data...
==> learn-packer.docker.ubuntu: Pulling Docker image: ubuntu:jammy
learn-packer.docker.ubuntu: jammy: Pulling from library/ubuntu
learn-packer.docker.ubuntu: Digest: sha256:eed7e1076bbc1f342c4474c718e5438af4784f59a4e88ad687dbb98483b59ee4
learn-packer.docker.ubuntu: Status: Image is up to date for ubuntu:jammy
learn-packer.docker.ubuntu: docker.io/library/ubuntu:jammy
==> learn-packer.docker.ubuntu: Starting docker container...
learn-packer.docker.ubuntu: Run command: docker run -v /Users/youruser/.packer.d/tmp761204751:/packer-files -d -i -t --entrypoint=/bin/sh -- ubuntu:jammy
learn-packer.docker.ubuntu: Container ID: ac8304190bdaa7890f0f1fa9b706dd53ad9aed141327300f11e4b79d949057b8
==> learn-packer.docker.ubuntu: Using docker communicator to connect: 172.17.0.2
==> learn-packer.docker.ubuntu: Provisioning with shell script: /var/folders/s6/m22_k3p11z104k2vx1jkqr2c0000gp/T/packer-shell164170680
learn-packer.docker.ubuntu: Adding file to Docker Container
==> learn-packer.docker.ubuntu: Committing the container
learn-packer.docker.ubuntu: Image ID: sha256:6e5eeb4a749ed48f01f10fa7f3a22c078a421f53166c9df461e6f08a23c2e327
==> learn-packer.docker.ubuntu: Killing the container: ac8304190bdaa7890f0f1fa9b706dd53ad9aed141327300f11e4b79d949057b8
Build 'learn-packer.docker.ubuntu' finished after 7 seconds 731 milliseconds.
==> Wait completed after 7 seconds 732 milliseconds
==> Builds finished. The artifacts of successful builds are:
--> learn-packer.docker.ubuntu: Imported Docker image: sha256:6e5eeb4a749ed48f01f10fa7f3a22c078a421f53166c9df461e6f08a23c2e327
In the output, you will find the Provisioning with shell script
that confirms that the Packer ran the provision step. Notice how Packer also outputs the first inline command (Adding file to Docker Container
).
Verify update image
Verify the image by running it with Docker. You'll need the IMAGE_ID
from the output of docker images
.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 6e5eeb4 31 seconds ago 134MB
To verify the provisioner created the example.txt
file in the new image, first launch the newly created Docker image. Replace IMAGE_ID
with the value shown above in your terminal (your local equivalent of 6e5eeb4
as shown in the output above).
$ docker run -it IMAGE_ID
In the Docker container shell, print the contents of the example.txt
file. This should return FOO is hello world
as expected.
$ cat example.txt
FOO is hello world
Type exit
to leave the container.
$ exit
Add more provisioners
The shell provisioner demonstrated above is extremely powerful and flexible. For complex provisioning, you can pass entire shell scripts rather than the inline
declarations shown above.
You can run as many provisioners as you'd like. Provisioners run in the order they are declared.
Replace your existing build block with the following. This adds a provisioner to the end of your build block that will print a message in the shell execution environment.
build {
name = "learn-packer"
sources = [
"source.docker.ubuntu"
]
provisioner "shell" {
environment_vars = [
"FOO=hello world",
]
inline = [
"echo Adding file to Docker Container",
"echo \"FOO is $FOO\" > example.txt",
]
}
provisioner "shell" {
inline = ["echo This provisioner runs last"]
}
}
Build and verify updated image
Build the image again.
$ packer build docker-ubuntu.pkr.hcl
learn-packer.docker.ubuntu: output will be in this color.
==> learn-packer.docker.ubuntu: Creating a temporary directory for sharing data...
==> learn-packer.docker.ubuntu: Pulling Docker image: ubuntu:jammy
learn-packer.docker.ubuntu: jammy: Pulling from library/ubuntu
learn-packer.docker.ubuntu: Digest: sha256:eed7e1076bbc1f342c4474c718e5438af4784f59a4e88ad687dbb98483b59ee4
learn-packer.docker.ubuntu: Status: Image is up to date for ubuntu:jammy
learn-packer.docker.ubuntu: docker.io/library/ubuntu:jammy
==> learn-packer.docker.ubuntu: Starting docker container...
learn-packer.docker.ubuntu: Run command: docker run -v /Users/youruser/.packer.d/tmp888485478:/packer-files -d -i -t --entrypoint=/bin/sh -- ubuntu:jammy
learn-packer.docker.ubuntu: Container ID: 1b62dad596bdca418f6c7ae6f0f36fa6e09ec1cedcc0ac00a71537d9955ae02c
==> learn-packer.docker.ubuntu: Using docker communicator to connect: 172.17.0.3
==> learn-packer.docker.ubuntu: Provisioning with shell script: /var/folders/s6/m22_k3p11z104k2vx1jkqr2c0000gp/T/packer-shell763320791
learn-packer.docker.ubuntu: Adding file to Docker Container
==> learn-packer.docker.ubuntu: Provisioning with shell script: /var/folders/s6/m22_k3p11z104k2vx1jkqr2c0000gp/T/packer-shell566489996
learn-packer.docker.ubuntu: This provisioner runs last
==> learn-packer.docker.ubuntu: Committing the container
learn-packer.docker.ubuntu: Image ID: sha256:4aeddd813656fe9c7c1675f0284f097cd0d98a58a319f167686ef594dec5b4eb
==> learn-packer.docker.ubuntu: Killing the container: 1b62dad596bdca418f6c7ae6f0f36fa6e09ec1cedcc0ac00a71537d9955ae02c
Build 'learn-packer.docker.ubuntu' finished after 10 seconds 366 milliseconds.
==> Wait completed after 10 seconds 366 milliseconds
==> Builds finished. The artifacts of successful builds are:
--> learn-packer.docker.ubuntu: Imported Docker image: sha256:4aeddd813656fe9c7c1675f0284f097cd0d98a58a319f167686ef594dec5b4eb
Notice how there are two Provisioning with shell-script
executions. The second provisioning step displays the expected message.
==> learn-packer.docker.ubuntu: Provisioning with shell script: /var/folders/...
learn-packer.docker.ubuntu: Adding file to Docker Container
==> learn-packer.docker.ubuntu: Provisioning with shell script: /var/folders/...
learn-packer.docker.ubuntu: This provisioner runs last
Next steps
In this tutorial, you used provisioners to set an environment variable and create a file in your Docker image. You can apply these same principles to provisioning and configuring any Packer images. Continue to the next tutorial to make your Packer template more robust with variables.
Refer to the following resources for additional details on the concepts covered in this tutorial:
- Read more about the Packer provisioners.
- Learn more about how to use Packer provisioner blocks.