Ansible Molecule Advanced: Multi-Instance, Delegated, and Docker Scenarios

Ansible Molecule Advanced: Multi-Instance, Delegated, and Docker Scenarios

Basic Molecule usage—spin up a Docker container, run a playbook, verify with Ansible—covers maybe 30% of real-world scenarios. When your roles provision multiple nodes, interact with cloud APIs, or require OS-specific behavior, you need the advanced toolkit. This guide covers multi-instance testing, the delegated driver, custom verifiers, and what Molecule 6.x changes.

Multi-Instance Scenarios

Most production playbooks touch more than one host. A web tier, a database tier, a load balancer. Testing roles in isolation misses the integration points. Molecule supports multi-instance scenarios through the platforms list.

# molecule/cluster/molecule.yml
driver:
  name: docker

platforms:
  - name: lb
    image: "geerlingguy/docker-ubuntu2204-ansible"
    groups:
      - loadbalancers
    networks:
      - name: cluster_net

  - name: web1
    image: "geerlingguy/docker-ubuntu2204-ansible"
    groups:
      - webservers
    networks:
      - name: cluster_net

  - name: web2
    image: "geerlingguy/docker-ubuntu2204-ansible"
    groups:
      - webservers
    networks:
      - name: cluster_net

  - name: db
    image: "geerlingguy/docker-rockylinux8-ansible"
    groups:
      - databases
    networks:
      - name: cluster_net

The converge.yml then targets groups just like a real inventory:

# molecule/cluster/converge.yml
---
- name: Configure load balancer
  hosts: loadbalancers
  roles:
    - haproxy

- name: Configure web servers
  hosts: webservers
  roles:
    - nginx
    - app_deploy

- name: Configure database
  hosts: databases
  roles:
    - postgresql

Container-to-container networking requires the shared network. Verify it's created before platforms try to attach:

# molecule/cluster/create.yml (override if needed)
# For Docker driver, networks defined in platforms auto-create.
# Add to molecule.yml under driver options if you need custom IPAM:
driver:
  name: docker
  options:
    managed: true

For the verify step, test cross-host behavior—not just that nginx is running, but that the load balancer can reach the web backends:

# molecule/cluster/verify.yml
---
- name: Verify cluster connectivity
  hosts: loadbalancers
  tasks:
    - name: Check HAProxy can reach web1
      uri:
        url: "http://web1:80/health"
        status_code: 200
      register: result

    - name: Assert web1 responded
      assert:
        that: result.status == 200

Delegated Driver for Cloud Testing

The Docker driver falls short when your role calls AWS APIs, provisions GCP resources, or expects actual cloud metadata. The delegated driver hands control to you—Molecule manages the test lifecycle but you own instance creation and deletion.

# molecule/aws/molecule.yml
driver:
  name: delegated

platforms:
  - name: app-server
    instance_raw_params:
      ami: ami-0c02fb55956c7d316
      instance_type: t3.micro
      subnet_id: "${SUBNET_ID}"
      key_name: "${KEY_NAME}"

You implement create.yml and destroy.yml:

# molecule/aws/create.yml
---
- name: Create EC2 instances
  hosts: localhost
  gather_facts: false
  vars:
    keypair_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
  tasks:
    - name: Generate keypair
      community.crypto.openssh_keypair:
        path: "{{ keypair_path }}"
        type: rsa
        size: 2048

    - name: Import keypair to AWS
      amazon.aws.ec2_key:
        name: "molecule-{{ lookup('env', 'MOLECULE_SCENARIO_NAME') }}"
        key_material: "{{ lookup('file', keypair_path + '.pub') }}"
        region: us-east-1

    - name: Launch instance
      amazon.aws.ec2_instance:
        name: "molecule-{{ item.name }}"
        instance_type: t3.micro
        image_id: ami-0c02fb55956c7d316
        key_name: "molecule-{{ lookup('env', 'MOLECULE_SCENARIO_NAME') }}"
        region: us-east-1
        wait: true
        tags:
          MoleculeScenario: "{{ lookup('env', 'MOLECULE_SCENARIO_NAME') }}"
      loop: "{{ molecule_yml.platforms }}"
      register: instances

    - name: Write instance config
      copy:
        content: "{{ instance_config | to_yaml }}"
        dest: "{{ molecule_instance_config }}"
      vars:
        instance_config: |
          {% for item in instances.results %}
          - instance: {{ item.instances[0].instance_id }}
            address: {{ item.instances[0].public_ip_address }}
            user: ec2-user
            port: 22
            identity_file: {{ keypair_path }}
          {% endfor %}

The MOLECULE_INSTANCE_CONFIG environment variable tells Molecule where to find the SSH connection details you wrote.

Custom Verifiers

Molecule's default verifier is Ansible, but sometimes you want Python tests (Testinfra), Go (Goss), or shell scripts.

Testinfra

# molecule/default/molecule.yml
verifier:
  name: testinfra
  options:
    sudo: true
# molecule/default/tests/test_nginx.py
import pytest

def test_nginx_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed
    assert nginx.version.startswith("1.")

def test_nginx_running(host):
    service = host.service("nginx")
    assert service.is_running
    assert service.is_enabled

def test_nginx_listening(host):
    socket = host.socket("tcp://0.0.0.0:80")
    assert socket.is_listening

def test_config_syntax(host):
    cmd = host.run("nginx -t")
    assert cmd.rc == 0
    assert "syntax is ok" in cmd.stderr

def test_default_page(host):
    cmd = host.run("curl -s http://localhost/")
    assert cmd.rc == 0
    assert cmd.stdout != ""

Goss for Fast Validation

Goss runs YAML-defined checks at ~10ms each—faster than Ansible tasks for pure validation:

# molecule/default/files/goss.yml
package:
  nginx:
    installed: true

service:
  nginx:
    enabled: true
    running: true

port:
  tcp:80:
    listening: true
    ip:
      - 0.0.0.0

file:
  /etc/nginx/nginx.conf:
    exists: true
    owner: root
    mode: "0644"
# In converge.yml, copy and run goss:
- name: Install and run goss
  hosts: all
  tasks:
    - name: Download goss
      get_url:
        url: https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64
        dest: /usr/local/bin/goss
        mode: "0755"

    - name: Copy goss spec
      copy:
        src: files/goss.yml
        dest: /tmp/goss.yml

    - name: Run goss
      command: goss -g /tmp/goss.yml validate --format tap
      register: goss_result
      changed_when: false

    - name: Assert goss passed
      assert:
        that: goss_result.rc == 0
        fail_msg: "{{ goss_result.stdout }}"

Converge Side Effects

Some roles have side effects that only appear after idempotency checks or when other roles run first. Molecule's side_effect.yml handles this:

# molecule/default/side_effect.yml
---
- name: Simulate service restart trigger
  hosts: all
  tasks:
    - name: Modify config to trigger handler
      lineinfile:
        path: /etc/app/config.ini
        line: "debug = true"
      notify: restart app

  handlers:
    - name: restart app
      service:
        name: myapp
        state: restarted

Enable it in molecule.yml:

provisioner:
  name: ansible
  playbooks:
    side_effect: side_effect.yml

Run with: molecule side-effect or it executes as part of the full molecule test sequence.

Molecule 6.x Features

Molecule 6 (released 2023) brought several workflow improvements.

Scenario inheritance lets you share a base molecule.yml and override per-scenario:

# molecule/default/molecule.yml
extends: ../../molecule/base.yml

platforms:
  - name: instance
    image: "geerlingguy/docker-ubuntu2204-ansible"

Parallel execution across scenarios (experimental, requires --parallel flag):

molecule test --all --parallel

Dependency caching via the dependency block now supports force: false to skip reinstalls when requirements haven't changed:

dependency:
  name: galaxy
  options:
    force: false
    requirements-file: requirements.yml

Matrix testing with environment variables lets CI test across multiple OS images without separate scenario directories:

# GitHub Actions matrix
strategy:
  matrix:
    image:
      - geerlingguy/docker-ubuntu2204-ansible
      - geerlingguy/docker-rockylinux9-ansible
      - geerlingguy/docker-debian12-ansible

steps:
  - name: Run Molecule
    <span class="hljs-built_in">env:
      MOLECULE_IMAGE: <span class="hljs-variable">${{ matrix.image }}
    run: molecule <span class="hljs-built_in">test
# molecule.yml reads the env var:
platforms:
  - name: instance
    image: "${MOLECULE_IMAGE:-geerlingguy/docker-ubuntu2204-ansible}"

Practical Workflow

For a role that's used across multiple OS versions and deployment targets, a complete Molecule setup looks like:

molecule/
  default/          # Docker, Ubuntu, quick iteration
  rocky/            # Docker, Rocky Linux, OS compat
  aws/              # Delegated, real EC2 instances
  cluster/          # Docker multi-instance

Run molecule test -s default during development. Run molecule test --all in CI. Gate the aws scenario on merges to main only—it takes 5+ minutes and costs real money.

The investment in multi-scenario Molecule setups pays off when you catch the "works on Ubuntu, broken on Rocky" bugs before they reach staging.

Read more