Testing automation: lint, check mode and Molecule
ansible-lint profiles and rules, --check and --diff with their limits, Molecule scenarios on containers, and running the whole thing in CI.
Linting the code
# .ansible-lint
profile: production # or: min, basic, moderate, safety
exclude_paths:
- .cache/
- collections/
- molecule/
warn_list:
- experimental
skip_list:
- jinja[spacing] # only if you disagree, and say why in a comment
use_default_rules: true
offline: falseansible-lint # human-readable
ansible-lint -f pep8 # machine-readable for CI
ansible-lint --fix # applies what it can safely rewrite
ansible-lint roles/web/tasks/main.yml
# the rules that catch the most real problems
# no-changed-when a command task without a change condition
# risky-shell-pipe a shell pipeline without pipefail
# command-instead-of-module using command where a module exists
# name[template] a task name built from variables- The
productionprofile is the strictest set of opt-in rules and is a reasonable target for shared roles. - Every
shelltask needsset -o pipefail, or a failing command earlier in the pipeline is masked by a later one that succeeds. - A task name containing a variable is fine for a loop with
loop_control.label, but lint will ask you to be explicit. - Lint is not a substitute for running the play. It cannot tell you that a package name is wrong for the target distribution.
Check mode and diff
# a dry run: report what would change, change nothing
ansible-playbook -i inventory site.yml --check --diff --limit web1
# check mode with a tag filter is the fastest useful review
ansible-playbook -i inventory site.yml --check --diff --tags config
# syntax and inventory checks that need no connection at all
ansible-playbook site.yml --syntax-check
ansible-inventory -i inventory/hosts.ini --list
ansible-inventory -i inventory/hosts.ini --graph| Task type | Check mode behaviour | Risk |
|---|---|---|
template, copy, file | Reports the change accurately | Low |
package | Usually accurate | Low |
service | Reports, does not act | Low |
command / shell | Skipped unless check_mode: false | Has side effects when forced |
uri with a write | Runs unless you guard it | Real data changes |
⚠️
Check mode is only as truthful as the modules you use. A
command task with check_mode: false genuinely executes during a dry run, and a task whose result a later when depends on will behave differently in check mode. Never treat a clean check run as proof a deploy is safe.Molecule and CI
# molecule/default/molecule.yml
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-2404
image: geerlingguy/docker-ubuntu2404-ansible:latest
pre_build_image: true
command: /usr/sbin/init
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
- name: debian-12
image: geerlingguy/docker-debian12-ansible:latest
pre_build_image: true
command: /usr/sbin/init
privileged: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
verify: verify.yml
inventory:
group_vars:
all:
app_version: "2.4.0"
verifier:
name: ansible
scenario:
test_sequence:
- dependency
- destroy
- create
- converge
- idempotence # runs converge twice and fails if the second changes anything
- verify
- destroy# molecule/default/verify.yml
- name: Verify
hosts: all
become: true
tasks:
- name: The service is listening
ansible.builtin.wait_for:
port: 8080
timeout: 30
- name: The health endpoint answers
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
- name: The config file has the expected owner and mode
ansible.builtin.stat:
path: /etc/app/app.env
register: env
failed_when: env.stat.mode != '0640' or env.stat.pw_name != 'app'# .github/workflows/ansible.yml
name: ansible
on: [push, pull_request]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install ansible-core==2.17.* ansible-lint molecule molecule-plugins[docker]
- run: ansible-galaxy collection install -r requirements.yml
- run: ansible-lint
- run: molecule test
working-directory: roles/webThe idempotence step is the most valuable part of a Molecule run: converging twice and failing on any change on the second pass is the only automated proof that your tasks converge instead of repeating themselves.
FAQ
Is Molecule worth it for a small role?
Yes for any role you will run more than once on more than one distribution. The idempotence check alone catches the mistakes that cause an unexpected restart in production.
Why does my task pass in check mode but fail for real?
A command or shell task is skipped in check mode, so a dependency it creates is never created, and the real run takes a different path. Check mode cannot validate tasks that shell out.
Related
Error handling, blocks and rescue Scaling: dynamic inventory and AWX
Last refreshed 2026-09-18.