How To Reuse Your Ansible Roles To Build Docker Images With Packer
Follow this step-by-step tutorial to learn how to reuse your Ansible roles to build Docker images using Packer.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I will show you how to reuse your Ansible roles to build Docker images using Packer.
If you are like me, you have probably used Ansible for a while. You probably have Ansible roles that you use to install and manage software on-premise and VM. Now you want to reuse those roles to build your own custom Docker images to use for development, testing, or production.
This is possible and very useful with minor changes to the Ansible roles. In fact, with Docker, you start with a very minimalistic environment and you can't take for granted some components that you will find preinstalled on major Linux distros.
Prerequisites
- You must know Ansible and have some scripts to run.
- You must have Docker and Packer installed. Refer to the respective sites in order to install them.
In my setup, I've used a CentOS 7 box with Docker and Packer installed, because I know for sure that my Ansible roles run fine on that distro.
To verify that they are correctly installed, you can issue the following commands on the CentOS 7 bash:
vagrant@centosDocker ~> docker --version
Docker version 20.10.6, build 370c289
vagrant@centosDocker ~> packer --version
1.7.2
Ansible Roles and Packer Files
My Ansible roles are organized following the standard Ansible conventions. I have a directory called "provision" in which I have my Ansible roles and Packer files.
For example, I will create a MariaDB version 10.4 Docker image customized for my needs. The directory structure is as follows:
├── packer_centos7_mariaDB104.json
├── packer_MariaDB104-playbook.yml
├── roles
│ ├── cleanup
│ │ ├── defaults
│ │ ├── files
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ ├── disable_firewalld
│ │ ├── defaults
│ │ │ └── main.yml
│ │ ├── files
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ ├── MariaDB104_multi
│ │ ├── comandi.txt
│ │ ├── defaults
│ │ │ └── main.yml
│ │ ├── files
│ │ │ ├── master.zip
│ │ │ └── mysqlinit.sql
│ │ ├── tasks
│ │ │ ├── main.yml
│ │ │ └── sub.yml
│ │ └── templates
│ │ ├── mariadb.service
│ │ ├── my.cnf
│ │ ├── mysqlinit.sh
│ │ ├── mysql.server
│ │ ├── mysql_wrapper
│ │ └── swap_porte_mysql
│ │ └── main.yml
.
.
.
├── vars
Let's explain the relevant files.
Packer Template JSON
packer_centos7_mariaDB104.json
is our entry point, and is a JSON file that describes what we are building and how (see this link).
This is the content of the file:
{
"builders": [
{
"name": "docker",
"type": "docker",
"image": "centos/systemd",
"commit": true,
"privileged": true,
"run_command": ["-d", "-i", "-t", "--entrypoint=/usr/sbin/init", "--", "{{.Image}}"]
}
],
"provisioners": [
{
"type": "shell",
"script": "packer_install_python.sh"
},
{
"type": "ansible",
"playbook_file": "packer_MariaDB104-playbook.yml"
}
],
"post-processors": [
{
"type": "docker-tag",
"repository": "mariadb104-image",
"tag": "1.0"
}
]
}
Where:
builders
: This contains an array of all the builders that Packer should use to generate machine images for the template. In this case, I'm using Docker to build my machine image. At this point, I've encountered my first problem with reusing my Ansible roles. The majority of my roles rely heavily on systemd
(in the case of RedHat derivatives like CentOS) to launch and control my services; for example, MySQL. On a standard CentOS 7 Docker image, systemd
is not present by default, because usually with Docker you start with a script for your single service.
To overcome this problem I've found two solutions:
- Use a Docker image with
systemd
installed like in the previous example. Note: In this case, the container must be run withprivileged
set totrue
. You must be very careful about this if you intend to use your image in production (see Docker documentation about this). - Use this script that replaces
systemctl
command and doesn't needprivileged
rule. I suggest always using this approach because it is more secure and more general. With the first method, you have to find a Docker image of your preferred distro withsystemd
or other service manager installed, or create your own.
provisioners
are the provisioners of our image. In this case, we have:
packer_install_python.sh
is a Shell script that Packer will run in order to install Python (used by Ansible).packer_MariaDB104-playbook.yml
is my Ansible playbook. The relevant aspect is that this is the same playbook that I use to install on bare-metal, Vagrant, and, thanks to Packer, everywhere!
Packer Template HCL2
As of Packer version 1.7.0, HCL2 is the preferred way to write Packer templates. You can use the hcl2_upgrade
command to transition your existing Packer JSON template to HCL2. See this.
We use the command hcl2_upgrade
to convert the previous file into HCL2 format:
packer hcl2_upgrade packer_centos7_mariaDB104.json
We obtain:
vagrant@centosDocker /v/c/a/provision> cat packer_centos7_mariaDB104.json.pkr.hcl
source "docker" "autogenerated_1" {
commit = true
image = "centos/systemd"
privileged = true
run_command = ["-d", "-i", "-t", "--entrypoint=/usr/sbin/init", "--", "{{ .Image }}"]
}
build {
sources = ["source.docker.autogenerated_1"]
provisioner "shell" {
script = "packer_install_python.sh"
}
provisioner "ansible" {
playbook_file = "packer_MariaDB104-playbook.yml"
}
post-processor "docker-tag" {
repository = "mariadb104-image"
tag = "1.0"
}
}
The generated HCL2 template file contains three blocks:
- A source block that defines the builder Packer will use to build the image
- A build block
- A composite block that defines what Packer will execute when running packer
build packer_centos7_mariaDB104.json.pkr.hcl
Packer Template HCL2 Update
We have run the hcl2_upgrade
command to automatically map and generate the relevant HCL2 block.
We rename these blocks to accurately represent and describe the resource:
- We add a
variable
block that defines the image variable. - We changed the
source
andbuild
blocks. - Since this Packer template uses the Docker
v0.0.7
plugin, we add therequired_plugins
Packer block to the top of the file. This ensures Packer will retrieve the Docker plugin that fulfills the version constraint, so we can consistently generate an image from this template. - We have to correct the
post-processor "docker-tag"
that is not accurately converted.
The final file is:
packer {
required_plugins {
docker = {
version = ">= 0.0.7"
source = "github.com/hashicorp/docker"
}
}
}
variable "image" {
type = string
default = "centos/systemd"
}
source "docker" "centos_systemd" {
commit = true
image = "${var.image}"
privileged = true
run_command = ["-d", "-i", "-t", "--entrypoint=/usr/sbin/init", "--", "{{ .Image }}"]
}
build {
sources = ["source.docker.centos_systemd"]
provisioner "shell" {
script = "packer_install_python.sh"
}
provisioner "ansible" {
playbook_file = "packer_MariaDB104-playbook.yml"
}
post-processor "docker-tag" {
repository = "mariadb104-image"
tag = ["1.1"]
}
}
Build the Image
First, we initialize the template.
vagrant@centosDocker /v/c/a/provision> packer init packer_centos7_mariaDB104.json.pkr.hcl
Installed plugin github.com/hashicorp/docker v1.0.3 in "/home/vagrant/.packer.d/plugins/github.com/hashicorp/docker/packer-plugin-docker_v1.0.3_x5.0_linux_amd64"
We can now build the image.
vagrant@centosDocker /v/c/a/provision> packer build packer_centos7_mariaDB104.json.pkr.hcl
docker.centos_systemd: output will be in this color.
==> docker.centos_systemd: Creating a temporary directory for sharing data...
==> docker.centos_systemd: Pulling Docker image: centos/systemd
docker.centos_systemd: Using default tag: latest
docker.centos_systemd: latest: Pulling from centos/systemd
docker.centos_systemd: Digest: sha256:09db0255d215ca33710cc42e1a91b9002637eeef71322ca641947e65b7d53b58
docker.centos_systemd: Status: Image is up to date for centos/systemd:latest
docker.centos_systemd: docker.io/centos/systemd:latest
==> docker.centos_systemd: Starting docker container...
...
docker.centos_systemd:
docker.centos_systemd: PLAY RECAP *********************************************************************
docker.centos_systemd: default : ok=61 changed=41 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
docker.centos_systemd:
==> docker.centos_systemd: Committing the container
docker.centos_systemd: Image ID: sha256:095b3d3c2b9435a0e5eab51b02a99df0cff2a24106b40afb1a17080363f43e2f
==> docker.centos_systemd: Killing the container: 5e98ca0d8b54c107682001f77ab5befc8f685b58b67db58b499079afe4f824fe
==> docker.centos_systemd: Running post-processor: (type docker-tag)
==> docker.centos_systemd (docker-tag): Deprecation warning: "tag" option has been replaced with "tags". In future versions of Packer, this configuration may not work. Please call `packer fix` on your template to update.
docker.centos_systemd (docker-tag): Tagging image: sha256:095b3d3c2b9435a0e5eab51b02a99df0cff2a24106b40afb1a17080363f43e2f
docker.centos_systemd (docker-tag): Repository: mariadb104-image:1.1
Build 'docker.centos_systemd' finished after 11 minutes 23 seconds.
==> Wait completed after 11 minutes 23 seconds
==> Builds finished. The artifacts of successful builds are:
--> docker.centos_systemd: Imported Docker image: sha256:095b3d3c2b9435a0e5eab51b02a99df0cff2a24106b40afb1a17080363f43e2f
--> docker.centos_systemd: Imported Docker image: mariadb104-image:1.1 with tags mariadb104-image:1.1
As you can see, Docker has invoked Ansible and provisioned the container from which a Docker image has been committed.
If we list the Docker images, we will find our newly created image ready to be used:
vagrant@centosDocker /v/c/a/provision> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mariadb104-image 1.1 095b3d3c2b94 28 minutes ago 3.68GB
That is all!
Opinions expressed by DZone contributors are their own.
Comments