Advanced 10 min · 2026-06-21

Ansible Linting & Molecule Testing: 5 Production Incidents That Forced Us to Get It Right

Master ansible-lint rules, yamllint, Molecule with Docker driver, Testinfra tests, and CI/CD integration.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Run ansible-lint --profile production to enforce production-level rules; profiles define severity thresholds. Use yamllint -c .yamllint to enforce YAML style consistency; common rules: line-length, indentation, truthy. Initialize a Molecule role with molecule init role --driver-name docker; the default scenario includes create, converge, verify, destroy. Write Testinfra tests in molecule/default/verify.yml or tests/test_default.py; use host.run() for command checks, host.file() for file assertions. Integrate lint and test in CI with ansible-lint . && molecule test; use --parallel for faster multi-OS testing. Common lint violations: name[missing] (tasks need names), no-changed-when (commands need changed_when), fqcn-builtins (use fully qualified collection names). Always pin molecule and ansible-lint versions in requirements.txt to avoid breaking changes. Use molecule converge to debug without full destroy cycle; molecule verify runs tests against running instances.

✦ Definition~90s read
What is Ansible Linting and Testing with Molecule?

Ansible Linting is the process of statically analyzing Ansible playbooks, roles, and inventories for syntax errors, best practice violations, and security issues. The primary tools are ansible-lint and yamllint. ansible-lint checks for over 100 rules, organized into profiles: min, basic, moderate, safety, shared, production.

Imagine you're a chef writing a complex recipe for a team of cooks.

Each profile enables a set of rules with increasing strictness. For example, the production profile flags missing task names (name[missing]), use of deprecated modules, and missing changed_when on commands. yamllint enforces YAML formatting consistency — indentation, line length, trailing spaces — which prevents subtle parsing errors.

Molecule is a testing framework for Ansible roles. It automates the lifecycle of creating a test environment (typically a Docker container), applying your role, verifying the state, and destroying the environment. The core commands are molecule create (provision instances), molecule converge (apply the role), molecule verify (run tests), and molecule destroy (teardown).

The Docker driver is the most popular for CI because it's fast, isolated, and requires no cloud credentials. Molecule supports multiple scenarios (e.g., default, ubuntu, centos) for testing across OS families.

Together, linting and testing form a safety net. Linting catches problems early, often in your editor. Testing validates behavior in a clean environment. In CI/CD, they run automatically, ensuring every role change is safe before it hits production.

Plain-English First

Imagine you're a chef writing a complex recipe for a team of cooks. You've got ingredients (variables), steps (tasks), and special tools (modules). But if your recipe has typos, missing steps, or inconsistent formatting, the dish will fail — maybe even start a kitchen fire. Linting is like having a sous chef who checks your recipe for common mistakes before anyone touches it. They flag missing 'cook for 10 minutes' instructions (changed_when) or secret ingredients you forgot to list (variables). Testing with Molecule is like doing a trial run in a practice kitchen. You set up a small, disposable kitchen (Docker container), follow your recipe step by step, and taste the result (verify tests). If something's off, you fix the recipe, not the real kitchen. This saves your restaurant from serving burnt food to customers — or in our case, deploying broken configs to production servers.

A few years ago, I was on-call for a major e-commerce platform. At 2 AM, our monitoring screamed: all checkout servers were returning 503s. I SSH'd in and found that a recent Ansible rollout had left the nginx config in a broken state — a missing semicolon in a template. The playbook ran without errors because the template module succeeded; it just rendered invalid config. We had no linting, no testing. That night, I vowed to never let that happen again. This article is the result of that incident and many more: the time ansible-lint saved us from a privilege escalation bug, the time Molecule caught a Docker image mismatch before prod deploy, the time a yamllint rule forced us to standardize YAML style across 50+ roles.

Historically, Ansible grew fast without a strong testing culture. Roles were shared as tarballs, and 'it works on my machine' was the norm. The community responded with ansible-lint (a static analyzer) and Molecule (a testing framework). These tools matured alongside Ansible itself, with ansible-lint now supporting profiles (production, safety, etc.) and Molecule integrating with Docker, Podman, Vagrant, and cloud drivers.

This article covers the complete pipeline: linting your Ansible code with ansible-lint and yamllint, testing roles with Molecule and the Docker driver, writing effective Testinfra and Ansible verify tests, and integrating everything into CI/CD. I'll share real production incidents, exact commands, and the gotchas that will save you hours of debugging.

Why ansible-lint Profiles Matter: From 'min' to 'production'

Ansible-lint profiles group rules by severity and context. The min profile (default) checks only syntax errors. basic adds style issues. moderate includes best practices. safety flags security concerns like hardcoded passwords. shared is for roles shared across teams. production is the strictest — it includes all safety rules plus requirements like name[missing] and no-changed-when.

To use a profile, create .ansible-lint: ``yaml --- profile: production ` Or pass via CLI: ansible-lint --profile production .`

In production, we use production profile in CI and moderate locally to avoid overwhelming devs. A common gotcha: production profile requires fqcn-builtins — you must use fully qualified collection names (e.g., ansible.builtin.copy instead of copy). Migrating legacy roles is painful but prevents namespace conflicts.

Example violation: name[missing] — every task must have a name. This is not just cosmetic; named tasks appear in output and enable --step mode. Use ansible-lint --fix to auto-add names (though it generates generic ones like 'Task 1').

Another critical rule: no-changed-when — commands must have changed_when to avoid reporting 'changed' every run. Without it, a command module always shows 'changed', breaking idempotency.

``yaml - name: Restart nginx ansible.builtin.command: systemctl restart nginx changed_when: false # or use a condition ``

Profile Switching Pitfall
When switching profiles, some rules may be new. Run ansible-lint --list-rules to see all rules and their profile. Use --warn-only to see violations without failing CI.
Production Insight
We once had a playbook that used shell instead of command because a dev didn't know the difference. The production profile flagged no-changed-when and command-instead-of-shell. We fixed it, but the real win was catching fqcn-builtins — our CI was using a community collection that shadowed builtins. That would have been a nightmare to debug.
Key Takeaway
Always use the production profile in CI; it enforces the strictest rules that prevent real production issues like missing changed_when and non-FQCN modules.

yamllint: The Unsung Hero of Ansible Maintainability

YAML is notoriously error-prone. A missing space or wrong indentation can break a playbook silently. yamllint catches these. Install: pip install yamllint. Create .yamllint at repo root: ``yaml --- extends: default rules: line-length: max: 120 indentation: spaces: 2 indent-sequences: consistent truthy: allowed-values: ['true', 'false', 'yes', 'no'] ` Run: yamllint .`

Common violations: line-length (default 80 is too short for Ansible tasks), truthy (YAML interprets 'on' as true), trailing-spaces (invisible but breaks diffs).

In production, we enforce yamllint in pre-commit hooks. A real incident: a junior engineer committed a playbook with mixed tabs and spaces. The playbook worked on their machine but failed in CI because the CI runner's YAML parser was stricter. yamllint caught it instantly.

Another gotcha: truthy rule flags true vs True. Stick to lowercase true/false. Use allowed-values: ['true', 'false'] to enforce.

For Ansible specifically, add ignore: | for files like .travis.yml or inventory that may not follow Ansible YAML conventions.

Pre-commit Integration
Add yamllint to .pre-commit-config.yaml: ``yaml - repo: https://github.com/adrienverge/yamllint.git rev: v1.32.0 hooks: - id: yamllint ``
Production Insight
We once had a YAML anchor (<<: *base) that worked locally but caused a parsing error in our CI's older PyYAML. yamllint didn't catch it because anchors are valid YAML. We had to pin PyYAML version. Lesson: yamllint checks syntax, not semantics.
Key Takeaway
yamllint prevents YAML parsing errors that can silently break deployments; always run it in CI and enforce consistent indentation and line length.

Molecule with Docker Driver: Setting Up for Role Testing

Molecule's Docker driver is the go-to for CI testing. Initialize a new role: molecule init role --driver-name docker myrole. This creates molecule/default/molecule.yml: ``yaml --- provisioner: name: ansible platforms: - name: instance image: geerlingguy/docker-ubuntu2204-ansible:latest pre_build_image: true ` Key configs: - pre_build_image: true avoids rebuilding the image each time, speeding up tests. - image` should include systemd or init system if testing services.

Lifecycle commands
  • molecule create — pulls image and starts container.
  • molecule converge — runs the role against the container.
  • molecule verify — runs tests (Testinfra or Ansible).
  • molecule destroy — tears down container.
  • molecule test — runs all four in sequence.

For testing multiple OSes, define multiple platforms: ``yaml platforms: - name: ubuntu image: geerlingguy/docker-ubuntu2204-ansible:latest - name: centos image: geerlingguy/docker-centos9-ansible:latest ` Molecule runs tests in parallel by default. Use --parallel` flag for speed.

Common gotcha: Docker driver requires Docker installed and the user in docker group. Also, pre_build_image: true images must have Ansible installed. Use geerlingguy/docker-*-ansible images — they are maintained and include Python and systemd.

Systemd in Containers
To test services, use an image with systemd (e.g., geerlingguy/docker-ubuntu2204-ansible). Add privileged: true and volumes: /sys/fs/cgroup:/sys/fs/cgroup:ro to molecule.yml platform config.
Production Insight
We used ubuntu:22.04 without Ansible pre-installed and wasted hours debugging 'python not found'. Switching to geerlingguy/docker-ubuntu2204-ansible:latest fixed it. Also, we forgot pre_build_image: true and molecule rebuilt the image every run — 5 minutes per test.
Key Takeaway
Always use pre-built Ansible images (like geerlingguy's) and set pre_build_image: true to keep test runs under 30 seconds.

Writing Testinfra Verify Tests: Beyond Simple Assertions

Testinfra is a Python framework for testing server state. Molecule runs tests in molecule/default/tests/test_default.py. Example: ```python import pytest

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

def test_nginx_config(host): config = host.file("/etc/nginx/nginx.conf") assert config.exists assert config.contains("worker_processes auto")

def test_nginx_listening(host): socket = host.socket("tcp://0.0.0.0:80") assert socket.is_listening `` Run with: molecule verify`.

Advanced patterns
  • Use host.ansible("debug", "var=nginx_config") to run ad-hoc Ansible modules.
  • Parametrize tests for multiple platforms: ``python @pytest.mark.parametrize("pkg", ["nginx", "python3"]) def test_packages(host, pkg): assert host.package(pkg).is_installed ``
  • Use host.run("curl -s http://localhost") to test HTTP endpoints.

Common mistake: Forgetting to import pytest. Also, test functions must start with test_ and take a host fixture.

For Ansible-based verify (instead of Testinfra), set verifier: ansible in molecule.yml and create verify.yml playbook. This is simpler for Ansible devs but less flexible.

Debugging Testinfra
Use molecule login to get a shell in the container, then run tests manually with pytest -v /root/tests/test_default.py to see detailed output.
Production Insight
We had a test that checked host.file("/etc/nginx/nginx.conf").contains("ssl_certificate"). It passed locally but failed in CI because the CI container had a different nginx config template. We had to parameterize the test to check for OS-specific paths.
Key Takeaway
Write idempotent tests that check behavior, not just file existence; use parametrization for multi-OS scenarios.

Ansible Verify Tests: When Testinfra Is Overkill

Molecule supports Ansible as a verifier. In molecule.yml, set verifier: name: ansible. Then create molecule/default/verify.yml: ``yaml --- - name: Verify hosts: all gather_facts: false tasks: - name: Check nginx is running ansible.builtin.command: systemctl is-active nginx changed_when: false register: nginx_status failed_when: nginx_status.stdout != "active" ` Run molecule verify`.

Pros: Familiar syntax for Ansible users; reuses modules. Cons: Slower than Testinfra; less expressive.

Use Ansible verify when you need to run complex module checks (e.g., uri module to test HTTP response). Example: ``yaml - name: Test web server response ansible.builtin.uri: url: http://localhost return_content: yes register: result failed_when: '"Welcome" not in result.content' ``

A gotcha: failed_when with register requires careful syntax. Use assert module for clarity: ``yaml - name: Assert nginx is active ansible.builtin.assert: that: - nginx_status.stdout == "active" ``

Verifier Performance
Ansible verifier runs the entire playbook, including gather_facts. Set gather_facts: false in verify.yml to speed up. Testinfra is generally faster for simple checks.
Production Insight
We used Ansible verify for a role that deployed a Java app. The test checked that the app responded on port 8080 with a specific JSON. Using uri module was straightforward. But we hit a timeout because the app took 30 seconds to start. We added sleep: 30 before the test — not ideal but worked.
Key Takeaway
Use Ansible verify when you need Ansible modules (like uri or mysql_query); use Testinfra for fast, Pythonic assertions.

Integrating Lint and Molecule in CI/CD: A Production Pipeline

A robust CI pipeline runs lint first, then test. Example GitHub Actions workflow: ``yaml name: Ansible CI on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - run: pip install ansible-lint yamllint - run: ansible-lint --profile production . - run: yamllint . test: needs: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - run: pip install molecule molecule-docker docker - run: molecule test ` Key points: - Separate lint and test jobs for parallel execution and faster feedback. - Pin tool versions in requirements.txt: ` ansible-lint==6.22.1 molecule==6.0.3 molecule-docker==2.1.0 yamllint==1.32.0 ` - Use molecule test --parallel for multi-platform tests. - Cache Docker images to speed up: docker pull geerlingguy/docker-ubuntu2204-ansible:latest` before molecule.

Common CI gotchas
  • Docker socket not mounted: Add -v /var/run/docker.sock:/var/run/docker.sock if using Docker-in-Docker.
  • Python version: Molecule requires Python 3.8+. Use actions/setup-python@v4.
  • Resource limits: Molecule with multiple platforms can consume >2GB RAM. Set runs-on: ubuntu-latest-8-cores or use --parallel carefully.
Pre-commit for Local Linting
Use pre-commit to run ansible-lint and yamllint before commit. This catches issues before CI. Add a .pre-commit-config.yaml with hooks for both tools.
Production Insight
We had a CI pipeline that ran molecule test without lint first. A PR with a syntax error (missing colon) passed lint locally because the dev used a different yamllint config. The molecule create step failed with a cryptic YAML error. We added lint as a required job before test and enforced the same config via .ansible-lint and .yamllint in the repo.
Key Takeaway
Always run lint before test in CI; pin tool versions to avoid environment discrepancies; use caching to speed up Docker image pulls.

Common Lint Violations and Why They Matter in Production

Here are the most common ansible-lint violations and their production impact:

  1. name[missing]: Tasks without names make playbooks unreadable and break --step mode. In a production outage, you need to know exactly which task is failing. Named tasks appear in logs and alerting.
  2. no-changed-when: Commands without changed_when always report 'changed'. This breaks idempotency checks — you can't trust the 'changed' count in Ansible Tower or AWX.
  3. fqcn-builtins: Using copy instead of ansible.builtin.copy can lead to module shadowing if a collection defines a copy module. In production, this caused a role to use a community copy module that behaved differently, corrupting files.
  4. command-instead-of-shell: Using shell when command suffices introduces shell injection risks. In a production incident, a shell task with user input allowed command injection, leading to a security breach.
  5. risky-file-permissions: Setting mode: 0777 on files is a security risk. Use 0644 or 0750.
  6. no-loop: Using with_items instead of loop is deprecated. Not a security issue, but prevents using loop_control features.

To fix these, run ansible-lint --fix . (only fixes some). Review each violation manually.

Rule Severity
Rules have severity levels: very high, high, medium, low. The production profile includes all very high and high rules. Use --show-severity to see them.
Production Insight
The fqcn-builtins violation once blocked a deployment because a role used service instead of ansible.builtin.service. The community.general collection was installed and its service module required extra parameters. We spent an hour debugging before ansible-lint flagged it.
Key Takeaway
Treat lint violations as production bugs; each rule prevents a real failure mode — from security breaches to idempotency breaks.

Molecule Scenarios: Testing Multiple Configurations

Molecule scenarios allow testing different configurations (e.g., different OS, different variables). Create molecule/ubuntu/molecule.yml and molecule/centos/molecule.yml. Run molecule test --scenario-name ubuntu.

Scenario structure: `` molecule/ default/ molecule.yml tests/ test_default.py ubuntu/ molecule.yml tests/ test_ubuntu.py ` Each scenario can have its own group_vars or host_vars`.

For variable testing, override variables in the scenario's molecule.yml: ``yaml provisioner: name: ansible inventory: group_vars: all: nginx_port: 8080 `` Then write tests that check port 8080.

Common use case: test the same role with different variables (e.g., ssl_enabled: true vs false). Create scenarios/ssl-enabled and scenarios/ssl-disabled.

Gotcha: Scenarios share the same tests/ directory unless you specify a custom path. Use verifier: name: testinfra directory: molecule/ubuntu/tests/ to point to scenario-specific tests.

Scenario Inheritance
Use molecule/default as base and override only what changes in other scenarios. This reduces duplication.
Production Insight
We had a role that behaved differently on Ubuntu vs CentOS due to package names. We created two scenarios and wrote separate tests. The CentOS scenario caught that we used httpd instead of apache2. Without it, the role would have broken on CentOS in production.
Key Takeaway
Use scenarios to test different OS families and variable combinations; this catches platform-specific bugs before deployment.

Advanced Molecule: Custom Drivers, Dependencies, and Pre/Post Tasks

Molecule supports drivers beyond Docker: Podman, Vagrant, EC2, GCE, Azure, and more. For production, we use the delegated driver to test against existing VMs. Example molecule.yml: ``yaml driver: name: delegated platforms: - name: my-prod-like-vm host: 10.0.0.1 `` But Docker is preferred for CI.

Dependencies are roles or collections required by your role. Define in molecule.yml: ``yaml dependency: name: galaxy options: requirements-file: requirements.yml ` Create requirements.yml: `yaml --- roles: - src: geerlingguy.nginx collections: - name: community.general ` Run molecule dependency` to install them.

Pre- and post-tasks allow running tasks before/after converge. Useful for preparing the environment (e.g., installing packages). Example: ``yaml provisioner: name: ansible playbooks: prepare: prepare.yml cleanup: cleanup.yml ` prepare.yml runs before converge; cleanup.yml` runs before destroy.

A production pattern: use prepare.yml to set up test fixtures (e.g., create a dummy database) and cleanup.yml to remove them.

Dependency Caching
Molecule downloads dependencies every run unless you set ANSIBLE_ROLES_PATH to a shared cache. Use ANSIBLE_ROLES_PATH=~/.ansible/roles to reuse.
Production Insight
We used the delegated driver to test a role against a staging VM that mirrored production. This caught a network dependency (a firewall rule) that Docker tests missed. But it was slow. We now use Docker for unit tests and delegated for integration tests.
Key Takeaway
Use Docker for fast, isolated testing; use delegated driver for production-like integration tests; manage dependencies via requirements.yml.

Debugging Molecule Failures: A Systematic Approach

  1. Check the log: molecule test --debug prints full Ansible output. Look for fatal: lines.
  2. Isolate the step: Run molecule create, molecule converge, molecule verify separately. If converge fails, the role has a bug. If verify fails, the test is wrong.
  3. Login to container: molecule login gives a shell. Run commands manually to verify state.
  4. Check Docker logs: docker logs <container_name> if the container exits immediately.
  5. Test with a simple playbook: Create a minimal playbook to rule out role issues.
Common failures
  • molecule create fails: Docker image not found or Docker daemon not running.
  • molecule converge fails: Ansible syntax error or missing variable.
  • molecule verify fails: Test assertion fails or test file syntax error.

Example: fatal: [instance]: FAILED! => {"msg": "The task includes an option with an undefined variable."}. The variable is missing from the role's defaults or the playbook. Check molecule/default/group_vars/all/.

Another: ERROR! 'community.general' is not installed. Run molecule dependency to install collections.

For Testinfra errors, import traceback: pytest --tb=long.

Molecule Log Files
Molecule stores logs in ~/.cache/molecule/<role>/<scenario>/logs/. Check converge.log and verify.log for detailed output.
Production Insight
We had a molecule converge failure that said 'no action detected in task'. The task used import_tasks with a relative path that didn't exist in the container. We fixed by using {{ role_path }}/tasks/.
Key Takeaway
Debug molecule step by step: use --debug, isolate phases, and log into the container to inspect state.

Idempotency Testing with Molecule: The 'converge twice' Pattern

Idempotency is critical for Ansible roles. Molecule can test it by running molecule converge twice. The first run applies the role; the second should report 'ok=0' changes. To automate, create a test: ``yaml # molecule/default/verify.yml - name: Idempotency check hosts: all gather_facts: false tasks: - name: Run role again ansible.builtin.include_role: name: myrole register: result - name: Assert no changes ansible.builtin.assert: that: - result is not changed ` Or in Testinfra: `python def test_idempotent(host): # This is tricky because Testinfra can't run roles easily. # Use host.ansible to run the role again. result = host.ansible("include_role", "name=myrole", check=False) assert result["changed"] == 0 ` Note: host.ansible` is experimental.

Common idempotency breakers
  • command/shell without changed_when.
  • lineinfile without regexp that changes the file every run.
  • template with different source every run (e.g., using date in template).

Production tip: Run idempotency test in CI for every PR. We once had a role that added a cron job every run because cron module didn't check for duplicates. Idempotency test caught it.

Idempotency with Docker
Docker containers may have different initial states (e.g., timezone). Use pre_build_image: true and a fixed image to ensure consistent results.
Production Insight
We had a role that used copy with a source file that was regenerated by a previous task. The second converge always changed the file because the source had a new timestamp. We fixed by using template with force: no.
Key Takeaway
Always test idempotency by running converge twice; catch tasks that report 'changed' every run and fix them with changed_when or idempotent modules.

Advanced ansible-lint: Custom Rules and Ignoring False Positives

Sometimes ansible-lint flags code that is intentionally written that way. You can ignore rules per file or per task. Use # noqa inline: ``yaml - name: Restart service ansible.builtin.command: systemctl restart nginx changed_when: false # noqa no-changed-when ` Or per file in .ansible-lint: `yaml skip_list: - fqcn-builtins # only if you have a good reason ` Better: use warn_list to see warnings without failing: `yaml warn_list: - experimental ``

Custom rules are Python scripts that implement a matchtask function. Example: a rule that forbids using apt module without update_cache: yes. Create rules/no_apt_without_update.py: ```python from ansiblelint.rules import AnsibleLintRule

class NoAptWithoutUpdate(AnsibleLintRule): id = "custom001" shortdesc = "apt must have update_cache: yes" description = "..." tags = ["custom"]

def matchtask(self, task, file=None): if task["action"]["__ansible_module__"] == "apt" and not task["action"].get("update_cache", False): return True `` Add to .ansible-lint: `yaml rules: - ./rules ``

Production insight: We wrote a custom rule to enforce that all file tasks have owner and group set. This prevented a security incident where a file was owned by root:root instead of app:app.

Custom Rule Performance
Custom rules run on every task, so keep them efficient. Use matchtask for task-level rules, matchplay for play-level.
Production Insight
We had a false positive from risky-file-permissions on a file that needed to be world-readable. We used # noqa risky-file-permissions with a comment explaining why. This documented the exception.
Key Takeaway
Use # noqa sparingly for intentional violations; write custom rules to enforce team-specific policies.
● Production incidentPOST-MORTEMseverity: high

Silent Variable Overwrite Breaks Production Config

Symptom
Application could not authenticate to database after a routine Ansible deployment. Error: 'FATAL: password authentication failed for user "app"'
Assumption
The engineer assumed the database credentials were correctly vaulted and that the playbook passed the correct variables.
Root cause
The role had a vars/main.yml with a default value for db_password (e.g., 'changeme'). The playbook used include_role with vars but forgot to pass db_password, so the default was used, overwriting the vaulted value.
Fix
Added private: true to the role's argument spec for db_password so that if not provided, ansible-lint (with safety profile) would flag it. Also added a assert task to fail if db_password equals 'changeme'.
Key lesson
  • Always define argument specs for role parameters and use ansible-lint's safety profile to catch missing required variables.
  • Never rely on defaults for secrets.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
ansible-lint exits with code 2 but no clear error message.
Fix
Run ansible-lint --verbose to see which rule is failing. Common: name[missing] if tasks lack names. Add name: to all tasks.
Symptom · 02
molecule converge succeeds but molecule verify fails on a file existence check.
Fix
The role may not have created the file. Use molecule login to inspect the container. Check the role's tasks for correct dest path. Add debug output.
Symptom · 03
molecule create fails with 'Docker connection error'.
Fix
Check if Docker is running: systemctl status docker. Ensure user is in docker group: sudo usermod -aG docker $USER. Re-login.
Symptom · 04
Testinfra test host.file('/etc/nginx/nginx.conf').exists returns False but file exists.
Fix
The test may be checking the wrong host. Verify the hostname in the test matches the container name. Use host.check_output('hostname').
★ Ansible Linting and Testing with Molecule Quick Referenceprint this for your desk
ansible-lint fails on 'name[missing]'
Immediate action
Add name to all tasks
Commands
ansible-lint --list-rules | grep name
ansible-lint --fix .
Fix now
Add name: "Describe task" to each task.
molecule test hangs on create+
Immediate action
Check Docker daemon
Commands
docker ps
molecule create --debug
Fix now
Restart Docker: sudo systemctl restart docker.
verify test fails on service status+
Immediate action
SSH into container
Commands
molecule login
systemctl status nginx
Fix now
Ensure service is enabled: systemctl enable nginx in role.
yamllint error: line too long+
Immediate action
Check .yamllint config
Commands
yamllint -d '{"extends": "default", "rules": {"line-length": {"max": 120}}}' .
Fix now
Add line-length: {max: 120} to .yamllint.
CI fails on molecule test but passes locally+
Immediate action
Check CI Docker version
Commands
docker --version
pip list | grep molecule
Fix now
Pin molecule version in requirements.txt: molecule==6.0.3
Ansible Linting and Testing Tools Comparison
ToolPurposeConfiguration FileKey Command
ansible-lintStatic analysis of Ansible code.ansible-lintansible-lint --profile production .
yamllintYAML syntax and style checking.yamllintyamllint .
MoleculeRole testing lifecyclemolecule.ymlmolecule test
TestinfraPython-based test assertionstests/test_default.pymolecule verify
Ansible VerifierAnsible playbook-based testsverify.ymlmolecule verify

Key takeaways

1
Use ansible-lint with the 'production' profile in CI to enforce strict rules that prevent real production failures.
2
yamllint prevents YAML parsing errors; enforce consistent indentation and line length in .yamllint.
3
Molecule with Docker driver provides fast, isolated role testing; always use pre-built images with pre_build_image
true.
4
Write Testinfra tests for fast, Pythonic assertions; use Ansible verifier for complex module-based checks.
5
Always run lint before test in CI to fail fast; pin tool versions to avoid environment discrepancies.
6
Test idempotency by running converge twice; fix tasks that always report 'changed'.
7
Use Molecule scenarios to test multiple OS families and variable combinations.
8
Write custom ansible-lint rules to enforce team-specific policies; use # noqa sparingly for intentional violations.

Common mistakes to avoid

6 patterns
×

Running molecule test without linting first

Symptom
CI fails late in pipeline after waiting for molecule create/converge
Fix
Run ansible-lint and yamllint as separate CI jobs before molecule test
×

Not pinning tool versions

Symptom
CI passes locally but fails in CI due to different version behavior
Fix
Pin versions in requirements.txt: ansible-lint==6.22.1, molecule==6.0.3
×

Using default ansible-lint profile (min) in CI

Symptom
Missing critical rule violations like no-changed-when or fqcn-builtins
Fix
Set profile: production in .ansible-lint
×

Not testing idempotency

Symptom
Role reports 'changed' every run, causing unnecessary restarts in production
Fix
Add a second converge step in molecule test or write an idempotency test
×

Forgetting to set pre_build_image: true

Symptom
Molecule builds Docker image every run, taking 2-5 minutes
Fix
Set pre_build_image: true in molecule.yml platforms
×

Writing verify tests that only check file existence, not content

Symptom
File exists but has wrong content, test passes, production breaks
Fix
Use .contains() or Ansible uri module to verify content
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between ansible-lint profiles 'min' and 'producti...
Q02SENIOR
How do you test an Ansible role against multiple Linux distributions usi...
Q03JUNIOR
What is the purpose of 'pre_build_image: true' in Molecule?
Q04JUNIOR
How do you write a Testinfra test to check that a service is running and...
Q05SENIOR
What is the 'fqcn-builtins' rule and why is it important?
Q06SENIOR
How do you ignore a specific ansible-lint rule for a single task?
Q07JUNIOR
What is the Molecule lifecycle and what commands correspond to each phas...
Q08SENIOR
How do you test idempotency with Molecule?
Q01 of 08SENIOR

What is the difference between ansible-lint profiles 'min' and 'production'?

ANSWER
'min' includes only syntax and basic sanity rules. 'production' includes all rules from 'safety' profile plus requirements like name[missing], no-changed-when, and fqcn-builtins. 'production' is the strictest profile intended for production-ready code.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is the difference between ansible-lint and yamllint?
02
Can I use Molecule without Docker?
03
How do I debug a failing Molecule test?
04
What is a common reason for molecule converge to fail?
05
How do I test a role that requires systemd?
06
What is the purpose of the 'no-changed-when' rule?
07
Can I run Molecule tests in parallel?
08
How do I enforce a custom policy (e.g., no hardcoded passwords) with ansible-lint?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
🔥

That's Ansible. Mark it forged?

10 min read · try the examples if you haven't

Previous
Ansible Performance Tuning
21 / 23 · Ansible
Next
Ansible Windows Automation