Create and Test Your Ansible Roles with Docker and Molecule
Article on the 11/20/2021 by Jules SAGOT
In this workshop/tutorial, we will deploy a SQL database (MariaDB) and a containerized web application (via Docker) with Ansible. We will also see how to use Molecule to generate an Ansible role and test it locally.
Prerequisites
To complete this tutorial, you should have installed Docker, Python3 and Pip.
The tutorial was done on Linux, so no promises if you’re using another operating system :wink:
Server Architecture
The goal of this Ansible project is to install Docker and MariaDB on a remote machine. The diagram above shows the test architecture of the project.
Molecule, Docker, and Ansible will be installed on your local machine (your computer). We will provision a Docker container in which Ansible will install our test application (i.e. MariaDB and our web app). The test container is the one on the left in the diagram.
This container will be given access to the Docker socket, which will allow it to launch the web application container. Note that this web app container will not be inside the test container but will sit alongside it.
The container isolating the web application will access MariaDB via a socket. To do this, we’ll use a shared volume between the test container and the web application container.
Creating an Ansible Role with Molecule
An Ansible role is a way to group a set of tasks to achieve a specific goal. Developing a set of reusable and tested roles helps avoid bugs and keeps deployment logic clean and modular.
Since our example is very simple, we’ll create a single role named app
. If your application grows in complexity, you can split the tasks of this role into multiple roles and create new ones for additional tasks your client might request.
For example, in my current project, I’ve split the deployment into four roles:
- Configuration of Docker, MariaDB, and creation of application containers
- Installation of Certbot (for SSL certificates) and Nginx (as a proxy to the containers)
- Installation of an FTP server
- Creation of a MariaDB user accessible remotely via a Qt application
Here’s how to create the project structure and install Ansible and Molecule in a Python virtual environment:
mkdir -p ansible-tutorial/roles
cd ansible-tutorial
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install ansible "molecule[docker,lint]"
Now we can create the structure of the app role using Molecule:
cd roles
molecule init role app --driver-name docker
You can view the structure of the created role using the tree app command. Here’s the output, which we’ll go through together:
app
├── defaults
│  └── main.yml
├── files
├── handlers
│  └── main.yml
├── meta
│  └── main.yml
├── molecule
│  └── default
│  ├── converge.yml
│  ├── molecule.yml
│  └── verify.yml
├── README.md
├── tasks
│  └── main.yml
├── templates
├── tests
│  ├── inventory
│  └── test.yml
└── vars
└── main.yml
Contents of the Created Ansible Role Folders
Tasks Directory
The tasks
directory contains the tasks to be executed when this role runs.
Variables Directory
The vars
and defaults
directories contain sets of variables.
Default variables should be placed in defaults
.
Variables related to specific scenarios can go into vars
.
Variables defined in defaults
can be overridden by those in vars
.
You can read more about Ansible variable precedence for a deeper understanding of how this works.
Molecule Directory
The molecule/default
folder inside the app
role contains two
Ansible playbooks:
converge.yml
verify.yml
The first playbook, converge.yml
, is executed using the command molecule converge
.
This runs the tasks of the current role using a test inventory (i.e. the test Docker container defined in molecule.yml
).
The second playbook, verify.yml
, is run using molecule verify
.
This playbook is intended to test the state of the test container to ensure the installation was successful.
Meta Directory
app/meta
contains a main.yml
file which describes the app
role.
You should update the author
and description
fields to make them meaningful and replace the author with the actual creator of the role.
You should also add the role_name
and namespace
fields.
galaxy_info:
role_name: app
namespace: tutorial
author: Jules Sagot--Gentil
description: |
Install MariaDB, Docker and deploy a demo app and a database.
Test Instance for Our Roles
We will modify the test instance provisioned by Ansible to make the Docker daemon accessible.
To do this, edit the file app/molecule/default/molecule.yml
.
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: tutorial-instance
image: debian:bullseye
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- instance-mariadb-socket:/var/run/mysqld
privileged: true
provisioner:
name: ansible
verifier:
name: ansible
Molecule will now run our Ansible role app
inside the Docker
container named tutorial-instance
, using the Debian Bullseye Docker
image.
The Docker volume named instance-mariadb-socket
will be used to
share the MariaDB socket between the containerized web application and
the tutorial-instance
container.
We can now launch the provisioning of the test container with:
cd app
molecule create
Installing Docker and MariaDB
We’ll edit the tasks/main.yml
file, which contains the tasks of our
role that will be executed by Ansible. To start, we’ll install Docker
and MariaDB.
---
- name: update packages
apt:
upgrade: yes
update_cache: yes
- name: install Docker dependencies
apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
- name: install Docker GPG key
apt_key:
data: "{{ lookup('file', 'docker_repo_gpg_key.asc') }}"
state: present
- name: add Docker's Debian repository
apt_repository:
repo: deb [arch=amd64] https://download.docker.com/linux/debian bullseye stable
state: present
update_cache: yes
- name: install Docker
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
update_cache: yes
- name: install Python pip
apt:
name:
- python3-pip
- name: install Python SDK for Docker
pip:
name: docker
- name: install MariaDB
apt:
name: mariadb-server
- name: /var/run/mysqld should belong to mysql
file:
path: /var/run/mysqld
owner: mysql
group: mysql
- name: start MariaDB service
service:
name: mariadb
state: started
enabled: yes
The files
directory contains static files required for the project.
You’ll need to download Docker’s GPG key to
install the software securely.
curl https://download.docker.com/linux/ubuntu/gpg > files/docker_repo_gpg_key.asc
We can now run molecule converge
to deploy our role on the Docker
test instance.
To verify that everything went as expected, we’ll check if we can run
the hello-world
container from within the test instance.
To do this, we’ll add the following task to the Ansible playbook
molecule/default/verify.yml
:
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: run docker hello world
community.docker.docker_container:
name: test_hello_world
image: hello-world
detach: no
cleanup: yes
To check if the tests pass, run molecule verify
.
Sharing and Using Standard Ansible Roles with Ansible Galaxy
The Ansible Galaxy project allows Ansible users to share their roles. For example, in our role, installing Docker takes four steps. There is likely already an Ansible Galaxy role that handles Docker installation.
Using roles from Ansible Galaxy helps modularize your Ansible project with roles that have been unit-tested and used in production by other Ansible users.
An Ansible Galaxy collection is a set of roles grouped by theme. We’re going to use the following two Ansible role collections:
community.docker
community.mysql
The first allows interaction with Docker containers, and the second provides ways to query a SQL database.
These role collections will be listed as a dependency of the app
role.
To do that, we’ll define a dependency file named requirements.yml
.
collections:
- name: community.docker
- name: community.mysql
To install these role collections (from the requirements.yml
file),
run the following command:
ansible-galaxy collection install -vr requirements.yml
Database Administration
The roles from the Ansible Galaxy collection community.mysql
that we
previously installed will allow us to create:
- A MariaDB user
- A database
Creating a MariaDB User
We’ll add a Jinja2 template for the MariaDB configuration file
my.cnf.j2
in the templates
folder. This template will store the
password for the MariaDB user created, ensuring that it can be reused
for future connections and maintain the role’s
idempotency.
[client]
user={{ root_user }}
password={{ root_password }}
The variables root_user
and root_password
will have default values
of demo
. We’ll store these defaults in defaults/main.yml
, along
with the name of the database to be created.
root_user: demo
root_password: demo
database: demo-db
Let’s add the creation of a MariaDB user to tasks/main.yml
:
#...
- name: install Python SDK for MariaDB
pip:
name: PyMySQL
- name: create a root user with password
community.mysql.mysql_user:
name: "{{ root_user }}"
password: "{{ root_password }}"
plugin: mysql_native_password
priv: '*.*:ALL,GRANT'
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: save MariaDB account
template:
src: my.cnf.j2
dest: /root/.my.cnf
owner: root
group: root
mode: '0600'
- name: remove the MariaDB root user
community.mysql.mysql_user:
name: root
state: absent
Creating a Database
We create a database using the mysql_db
role from the
community.mysql
Galaxy collection that was downloaded previously:
#...
- name: Database creation
community.mysql.mysql_db:
name: "{{ database }}"
state: present
Instantiating a Web Application
For this tutorial, we’ll simply deploy PHPMyAdmin, which is an open-source software that allows you to view and modify a database.
This time, we’ll use the Ansible Galaxy role collection
community.docker
, specifically the container
role.
The following new task should be added to tasks/main.yml
:
#...
- name: instantiate the web application
community.docker.docker_container:
name: webapp
image: phpmyadmin:5-apache
env:
PMA_SOCKET: "/var/run/mysqld/mysqld.sock"
PMA_HOST: localhost
PMA_USER: "{{ root_user }}"
PMA_PASSWORD: "{{ root_password }}"
volumes: "instance-mariadb-socket:/var/run/mysqld"
ports:
- "3000:80"
We also add a test to check if PHPMyAdmin is responding to requests on port 3000.
The following code should be added to the tasks of the playbook
verify.yml
, which will be executed when testing our Ansible role
with molecule verify
.
- name: the web application should respond to requests
command: "curl -s http://localhost:3000/"
delegate_to: localhost
Source Code of the Tutorial
The source code we wrote together in this tutorial is available on Gitlab.
Conclusion
This tutorial gave you an overview of the possibilities offered by Ansible roles in terms of code modularity and quality.
Molecule and Docker, on their side, provide a standardized way to deploy applications (by containerizing them) and the ability to test the correct execution of an Ansible role.
Automating deployment tests with Ansible, Molecule, and Docker allows for confidence during production releases, helps avoid human errors, and ensures traceability of the infrastructure.