Set up comprehensive testing for Ansible playbooks using Molecule framework and TestInfra validation. Create automated test scenarios, integrate with CI/CD pipelines, and ensure infrastructure code reliability in production environments.
Prerequisites
- Python 3.9 or higher
- Docker installed and running
- Basic Ansible knowledge
- Git version control system
What this solves
Ansible testing with Molecule and TestInfra provides automated validation for your infrastructure code before deploying to production. This prevents configuration drift, catches errors early, and ensures playbooks work consistently across different environments and operating systems.
Step-by-step installation
Update system packages and install Python dependencies
Start by updating your system and installing Python development tools required for Molecule and TestInfra.
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-dev python3-venv git docker.io
Configure Docker permissions
Add your user to the docker group to run containers without sudo. This is required for Molecule's Docker driver.
sudo usermod -aG docker $USER
newgrp docker
Create Python virtual environment for testing tools
Set up an isolated Python environment to avoid conflicts with system packages.
mkdir -p ~/ansible-testing
cd ~/ansible-testing
python3 -m venv molecule-env
source molecule-env/bin/activate
Install Ansible, Molecule, and TestInfra
Install the core testing framework components with Docker driver support.
pip install --upgrade pip
pip install ansible molecule[docker] testinfra yamllint ansible-lint
pip install molecule-plugins[docker]
Verify installation
Check that all tools are installed correctly and can communicate with Docker.
molecule --version
ansible --version
testinfra --version
docker version
Step-by-step configuration
Create sample Ansible role structure
Initialize a new Ansible role that we'll use to demonstrate testing capabilities.
cd ~/ansible-testing
molecule init role nginx_role --driver-name docker
Configure the sample Ansible role
Create a basic nginx installation role to test against multiple operating systems.
---
- name: Update package cache (Ubuntu/Debian)
apt:
update_cache: yes
when: ansible_os_family == "Debian"
- name: Update package cache (RHEL/CentOS)
dnf:
update_cache: yes
when: ansible_os_family == "RedHat"
- name: Install nginx
package:
name: nginx
state: present
- name: Start and enable nginx
systemd:
name: nginx
state: started
enabled: yes
- name: Create custom index page
copy:
content: |
Test Server
Nginx Test Server Running
dest: /var/www/html/index.html
owner: root
group: root
mode: '0644'
notify: restart nginx
Create role handlers
Define handlers for service restarts that will be triggered by configuration changes.
---
- name: restart nginx
systemd:
name: nginx
state: restarted
Configure Molecule scenarios
Set up Molecule to test against multiple Linux distributions with comprehensive test scenarios.
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-24
image: ubuntu:24.04
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: "/lib/systemd/systemd"
tmpfs:
- /run
- /tmp
- name: debian-12
image: debian:12
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: "/lib/systemd/systemd"
tmpfs:
- /run
- /tmp
- name: almalinux-9
image: almalinux:9
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: "/usr/lib/systemd/systemd"
tmpfs:
- /run
- /tmp
provisioner:
name: ansible
config_options:
defaults:
interpreter_python: auto_silent
callback_whitelist: profile_tasks, timer, yaml
ssh_connection:
pipelining: false
verifier:
name: testinfra
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy
Create prepare playbook for container setup
Configure containers to properly run systemd services during testing.
---
- name: Prepare containers
hosts: all
gather_facts: false
tasks:
- name: Install systemd and python3 (Ubuntu/Debian)
raw: |
apt-get update
apt-get install -y systemd python3 python3-apt
when: ansible_os_family == "Debian" or inventory_hostname is match("ubuntu.") or inventory_hostname is match("debian.")
changed_when: false
- name: Install systemd and python3 (RHEL/CentOS/AlmaLinux)
raw: |
dnf install -y systemd python3 python3-dnf
when: ansible_os_family == "RedHat" or inventory_hostname is match("alma.") or inventory_hostname is match("rocky.")
changed_when: false
- name: Gather facts after python installation
setup:
Create comprehensive TestInfra validation tests
Define infrastructure tests that verify nginx installation, configuration, and service status.
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def test_nginx_package_installed(host):
"""Test that nginx package is installed"""
nginx = host.package("nginx")
assert nginx.is_installed
def test_nginx_service_running(host):
"""Test that nginx service is running and enabled"""
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_listening_port_80(host):
"""Test that nginx is listening on port 80"""
socket = host.socket("tcp://0.0.0.0:80")
assert socket.is_listening
def test_nginx_config_valid(host):
"""Test that nginx configuration is valid"""
cmd = host.run("nginx -t")
assert cmd.rc == 0
assert "syntax is ok" in cmd.stderr
assert "test is successful" in cmd.stderr
def test_custom_index_page_exists(host):
"""Test that custom index page was created"""
index_file = host.file("/var/www/html/index.html")
assert index_file.exists
assert index_file.user == "root"
assert index_file.group == "root"
assert index_file.mode == 0o644
assert "Test Server Running" in index_file.content_string
def test_nginx_serves_custom_page(host):
"""Test that nginx serves the custom index page"""
cmd = host.run("curl -s http://localhost/")
assert cmd.rc == 0
assert "Test Server Running" in cmd.stdout
assert "Test Server " in cmd.stdout
def test_nginx_process_running(host):
"""Test that nginx processes are running"""
nginx_processes = host.process.filter(comm="nginx")
assert len(nginx_processes) >= 1
def test_nginx_log_files_exist(host):
"""Test that nginx log files exist and are writable"""
access_log = host.file("/var/log/nginx/access.log")
error_log = host.file("/var/log/nginx/error.log")
# Files should exist (created by package or after first request)
if access_log.exists:
assert access_log.user == "root"
if error_log.exists:
assert error_log.user == "root"
Create converge playbook
Define the main playbook that Molecule will use to test role execution.
---
- name: Converge
hosts: all
tasks:
- name: "Include nginx_role"
include_role:
name: "nginx_role"
Configure linting rules
Set up yamllint and ansible-lint configurations for code quality checks.
extends: default
rules:
line-length:
max: 120
level: warning
comments-indentation: disable
comments: disable
document-start: disable
Run complete Molecule test suite
Execute the full test sequence including syntax, idempotence, and infrastructure validation.
cd nginx_role
molecule test
Step-by-step CI/CD integration
Create GitHub Actions workflow
Set up automated testing in CI/CD pipelines with matrix builds for multiple distributions.
---
name: Molecule CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
scenario:
- default
python-version:
- '3.9'
- '3.10'
- '3.11'
steps:
- name: Check out code
uses: actions/checkout@v4
with:
path: 'nginx_role'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install molecule[docker] testinfra yamllint ansible-lint
pip install molecule-plugins[docker]
- name: Run Molecule tests
run: |
cd nginx_role
molecule test --scenario-name ${{ matrix.scenario }}
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
MOLECULE_NO_LOG: 'false'
Create GitLab CI configuration
Configure GitLab CI/CD pipeline for organizations using GitLab for version control and automation.
---
stages:
- test
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
cache:
paths:
- .cache/pip/
- venv/
molecule-test:
stage: test
image: python:3.11
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "/certs/client"
before_script:
- apt-get update && apt-get install -y docker.io
- python -m venv venv
- source venv/bin/activate
- pip install --upgrade pip
- pip install molecule[docker] testinfra yamllint ansible-lint
- pip install molecule-plugins[docker]
script:
- cd nginx_role
- molecule test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
lint-code:
stage: test
image: python:3.11
before_script:
- pip install yamllint ansible-lint
script:
- yamllint nginx_role/
- ansible-lint nginx_role/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Create Jenkins pipeline configuration
Set up Jenkins declarative pipeline for teams using Jenkins for continuous integration.
pipeline {
agent any
environment {
VENV_PATH = "${WORKSPACE}/molecule-venv"
PATH = "${VENV_PATH}/bin:${PATH}"
}
stages {
stage('Setup') {
steps {
script {
sh '''
python3 -m venv ${VENV_PATH}
. ${VENV_PATH}/bin/activate
pip install --upgrade pip
pip install molecule[docker] testinfra yamllint ansible-lint
pip install molecule-plugins[docker]
'''
}
}
}
stage('Lint') {
steps {
script {
sh '''
. ${VENV_PATH}/bin/activate
yamllint nginx_role/
ansible-lint nginx_role/
'''
}
}
}
stage('Test') {
steps {
script {
sh '''
. ${VENV_PATH}/bin/activate
cd nginx_role
molecule test
'''
}
}
}
}
post {
always {
script {
sh '''
. ${VENV_PATH}/bin/activate || true
cd nginx_role || true
molecule cleanup || true
molecule destroy || true
'''
}
}
}
}
Verify your setup
Run these commands to verify that your Molecule testing environment is working correctly.
cd ~/ansible-testing/nginx_role
source ../molecule-env/bin/activate
molecule --version
molecule list
molecule syntax
molecule create
molecule converge
molecule verify
molecule destroy
molecule list command should show your configured platforms (ubuntu-24, debian-12, almalinux-9).Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Docker permission denied | User not in docker group | sudo usermod -aG docker $USER && newgrp docker |
| Systemd services fail in containers | Missing privileged mode or cgroup mounts | Ensure privileged: true and proper volume mounts in molecule.yml |
| Python module import errors | Missing dependencies or wrong virtual environment | pip install molecule-plugins[docker] and activate virtual environment |
| Container creation timeout | Slow Docker image download | Pre-pull images: docker pull ubuntu:24.04 debian:12 almalinux:9 |
| Idempotence test failures | Tasks not properly idempotent | Add changed_when: false or proper conditionals to tasks |
| TestInfra import errors | Missing test file or wrong path | Ensure test file is in molecule/default/tests/ directory |
Next steps
- Install and configure Ansible with automation best practices and security
- Install and configure GitLab CE with CI/CD runners and backup automation
- Configure Ansible Vault for secret management and encryption
- Implement Ansible AWX Tower for enterprise automation workflows
- Setup Ansible dynamic inventory for AWS, Azure, and GCP cloud infrastructure
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Default values
INSTALL_DIR="$HOME/ansible-testing"
VENV_NAME="molecule-env"
# Usage message
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -d, --dir DIR Installation directory (default: $INSTALL_DIR)"
echo " -v, --venv NAME Virtual environment name (default: $VENV_NAME)"
echo " -h, --help Show this help message"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-d|--dir)
INSTALL_DIR="$2"
shift 2
;;
-v|--venv)
VENV_NAME="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}"
usage
;;
esac
done
# Error handling
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
if [[ -d "$INSTALL_DIR" ]]; then
rm -rf "$INSTALL_DIR"
fi
exit 1
}
trap cleanup ERR
# Logging function
log() {
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"
}
success() {
echo -e "${GREEN}✓ $1${NC}"
}
warning() {
echo -e "${YELLOW}⚠ $1${NC}"
}
error() {
echo -e "${RED}✗ $1${NC}"
}
# Check prerequisites
check_prerequisites() {
log "[1/8] Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
error "This script should not be run as root"
exit 1
fi
if ! command -v sudo &> /dev/null; then
error "sudo is required but not installed"
exit 1
fi
success "Prerequisites check passed"
}
# Detect distribution
detect_distro() {
log "[2/8] Detecting distribution..."
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution: /etc/os-release not found"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_UPGRADE="apt upgrade -y"
PKG_INSTALL="apt install -y"
PYTHON_PACKAGES="python3-pip python3-dev python3-venv git docker.io"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_UPGRADE="dnf update -y"
PKG_INSTALL="dnf install -y"
PYTHON_PACKAGES="python3-pip python3-devel git docker"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_UPGRADE="dnf update -y"
PKG_INSTALL="dnf install -y"
PYTHON_PACKAGES="python3-pip python3-devel git docker"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_UPGRADE="yum update -y"
PKG_INSTALL="yum install -y"
PYTHON_PACKAGES="python3-pip python3-devel git docker"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
success "Detected $PRETTY_NAME"
}
# Update system packages
update_system() {
log "[3/8] Updating system packages..."
sudo $PKG_UPDATE
sudo $PKG_UPGRADE
success "System packages updated"
}
# Install dependencies
install_dependencies() {
log "[4/8] Installing dependencies..."
sudo $PKG_INSTALL $PYTHON_PACKAGES
# Start and enable Docker
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
sudo systemctl enable --now docker
else
sudo systemctl enable --now docker
fi
success "Dependencies installed"
}
# Configure Docker permissions
configure_docker() {
log "[5/8] Configuring Docker permissions..."
sudo usermod -aG docker $USER
# Test if we can run docker commands without sudo
if ! docker version &>/dev/null; then
warning "Docker permission changes require logout/login to take effect"
warning "Run 'newgrp docker' or logout and login again"
fi
success "Docker permissions configured"
}
# Create Python virtual environment
create_virtualenv() {
log "[6/8] Creating Python virtual environment..."
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
python3 -m venv "$VENV_NAME"
source "$VENV_NAME/bin/activate"
# Upgrade pip
pip install --upgrade pip
success "Python virtual environment created at $INSTALL_DIR/$VENV_NAME"
}
# Install Python packages
install_python_packages() {
log "[7/8] Installing Ansible, Molecule, and TestInfra..."
cd "$INSTALL_DIR"
source "$VENV_NAME/bin/activate"
# Install core packages
pip install ansible molecule[docker] testinfra yamllint ansible-lint
pip install molecule-plugins[docker]
success "Python packages installed"
}
# Verify installation
verify_installation() {
log "[8/8] Verifying installation..."
cd "$INSTALL_DIR"
source "$VENV_NAME/bin/activate"
# Check versions
echo "Molecule version:"
molecule --version
echo ""
echo "Ansible version:"
ansible --version
echo ""
echo "TestInfra version:"
testinfra --version
echo ""
echo "Docker version:"
docker version --format "{{.Client.Version}}"
echo ""
success "Installation verification completed"
}
# Create activation script
create_activation_script() {
cat > "$INSTALL_DIR/activate.sh" << 'EOF'
#!/bin/bash
# Source this script to activate the molecule environment
# Usage: source activate.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/molecule-env/bin/activate"
echo "Molecule environment activated"
echo "Run 'deactivate' to exit the environment"
EOF
chmod 755 "$INSTALL_DIR/activate.sh"
success "Created activation script at $INSTALL_DIR/activate.sh"
}
# Main execution
main() {
echo -e "${GREEN}=== Ansible Molecule + TestInfra Installation ===${NC}"
echo ""
check_prerequisites
detect_distro
update_system
install_dependencies
configure_docker
create_virtualenv
install_python_packages
verify_installation
create_activation_script
echo ""
echo -e "${GREEN}=== Installation Complete ===${NC}"
echo ""
echo "To get started:"
echo " cd $INSTALL_DIR"
echo " source activate.sh"
echo " molecule init role my_role --driver-name docker"
echo ""
echo "Or manually activate the environment:"
echo " cd $INSTALL_DIR"
echo " source $VENV_NAME/bin/activate"
echo ""
warning "Note: You may need to logout/login or run 'newgrp docker' for Docker permissions to take effect"
}
main "$@"
Review the script before running. Execute with: bash install.sh