Linting, Molecule and Meaningful Verification
Scenario: A role passed review and worked in staging. In production it wrote a config file successfully and the service never started, because the rendered file was syntactically invalid for that OS version.
New words, in plain English
ansible-lint- a static analyser for playbooks and roles - style, correctness and safety rules.- Molecule - the role test framework: create, converge, idempotence, verify, destroy.
- Verifier - the assertion stage of a Molecule scenario.
- Static analysis - finding problems without running anything.
Testing Ansible has three layers, and each catches a different class of problem.
Static analysis runs in seconds and needs no infrastructure. ansible-lint catches shell where a module exists, command without changed_when, unnamed tasks, deprecated syntax, unsafe file permissions and much more. --syntax-check catches structural YAML errors. Both belong on every pull request; they replace a lot of review comments that a machine can make instead of a person.
Molecule runs the role against a real, disposable environment. Its stages are fixed: create builds the environment, converge applies the role, idempotence runs it again and fails on any change, verify runs your assertions, destroy cleans up. molecule test runs the whole sequence.
Verification is where the value concentrates and where most teams stop short. Asserting that a file exists proves the task ran. Asserting that the service is listening on the expected port, that its own parser accepts the config, and that a health endpoint answers proves the role worked. The scenario is exactly this gap: the file existed, and nothing checked that the service could use it.
Molecule scenarios can also test multiple platforms - a Debian and a RedHat container in the same run - which is the practical way to keep multi-OS roles honest.
Analogy: Lint is spell-check. Converge is printing the document. Idempotence is checking that printing it twice does not produce two documents. Verify is reading it to see whether it says anything true - and it is the step everyone skips.
A worked example
# Static - fast, on every pull request
ansible-lint
ansible-playbook site.yml --syntax-check
# molecule/default/molecule.yml
---
driver:
name: docker
platforms:
- name: ubuntu
image: ubuntu:22.04
- name: rocky
image: rockylinux:9
provisioner:
name: ansible
verifier:
name: ansible
# molecule/default/verify.yml - assert BEHAVIOUR, not file existence
- name: Verify
hosts: all
tasks:
- name: The service is listening
ansible.builtin.wait_for:
port: 8080
timeout: 10
- name: Its own parser accepts the rendered config
ansible.builtin.command: nginx -t
changed_when: false
- name: The health endpoint answers
ansible.builtin.uri:
url: http://localhost:8080/healthz
status_code: 200
# molecule test -> create, converge, idempotence, verify, destroy
ansible-lint is configurable through .ansible-lint, and it is worth tuning rather than either ignoring it or accepting every rule unexamined. Some rules are strong safety signals; a few are stylistic. Agree the set once, put it in the repository, and let CI enforce it consistently.
This is the same philosophy the labs in this track use. Their checkers do not confirm that you wrote a file - they run your playbook, run it again to prove idempotency, and then inspect the real end state on the managed hosts over SSH. Passing requires the automation to actually work, not merely to run.
Tip: Write the verify stage first, before the role. It forces you to state what "working" means before you start guessing at tasks.
Goal: Put this to work in the ansible-check-mode-safety lab. Open/labs/ansible, pickansible-check-mode-safety, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.