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

Using Docker and MariaDB with Ansible

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.