Ansible in depth · Lesson 5 · 35 min

Testing & linting your automation

A playbook that runs against your whole fleet deserves the same care as any other code. Before it goes near production you want to know: does it parse? Does it follow good habits? Does a second run change nothing? Does the server actually work afterwards? This lesson is the toolbox for those four questions, and ends with the checks you'd put in a CI pipeline.

You will learn

  • --syntax-check, --check --diff, --list-tasks
  • ansible-lint: the rules behind its messages, and profiles
  • The idempotence test: run twice, expect changed=0
  • Smoke tests with uri + assert
  • Tags: run only part of a playbook
  • Molecule, and Ansible checks in CI

Four levels of checking

CheckCommandCatches
Syntaxansible-playbook site.yml --syntax-checkYAML mistakes, unknown modules, broken structure. Seconds, no servers needed.
Lintansible-lintRisky or sloppy patterns: no mode on files, commands that always say changed, state: latest, unnamed tasks
Idempotencerun the playbook twiceTasks that change things every time. The second run must say changed=0.
Smoke testa test playbook with uri + assert“It ran fine, but the site is down”

ansible-lint

Rocky / RHEL
sudo dnf install epel-release
sudo dnf install ansible-lint
Ubuntu / Debian
sudo apt install ansible-lint

Either way you can also get the newest version with pipx install ansible-lint. Run it in the project folder:

WARNING  Listing 7 violation(s) that are fatal
fqcn[action-core]: Use FQCN for builtin module actions (package).
roles/web/tasks/main.yml:2 Use `ansible.builtin.package` or `ansible.legacy.package` instead.
…
Failed: 7 failure(s), 0 warning(s) on 8 files. Last profile that met the validation criteria was 'min'.
RuleWhat it meansFix
fqcn[action-core]short module namepackage: → ansible.builtin.package:
name[missing] / name[casing]unnamed task / lowercase namegive every task a Capitalised name
yaml[truthy]yes/nouse true/false
risky-file-permissionstemplate/copy without modemode: "0644" (quoted!)
package-lateststate: latest upgrades whenever the playbook runsstate: present, and upgrade on purpose
no-changed-whena command that always reports changedchanged_when:, creates:, or a real module

Profiles are levels, from min to production. The last line tells you the highest level your code fully meets, which is handy for setting a goal: “this repo must pass production”.

The idempotence test

Run the playbook twice in a row. The second recap must be all changed=0. If a task keeps changing (like shell: date > deployed.txt), it's either doing needless work or hiding a real difference between runs. Either way, fix it or delete it, because a playbook that always “changes” makes real changes impossible to spot.

Smoke tests

- name: Smoke-test the web servers
  hosts: web
  gather_facts: false
  tasks:
    - name: Fetch the home page from the control machine
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}/"
        return_content: true
      delegate_to: localhost
      register: page
    - name: The page has the right title
      ansible.builtin.assert:
        that:
          - page.status == 200
          - "site_title in page.content"

Tags: just the part you need

- name: Put our home page in place
  ansible.builtin.template: …
  tags: content

ansible-playbook site.yml --list-tasks          # what would run, with tags
ansible-playbook site.yml --tags content        # only tasks tagged content
ansible-playbook site.yml --skip-tags content

Handy for a quick content update, but always run the full playbook regularly too, so nothing drifts.

In CI and beyond

Put the cheap checks in your CI pipeline (from the DevOps path) so every push is checked:

steps:
  - uses: actions/checkout@v4
  - run: pip install ansible-core ansible-lint
  - run: ansible-playbook site.yml --syntax-check
  - run: ansible-lint

The next step up is Molecule: it starts throw-away containers (Rocky and Ubuntu, if you like), applies your role, runs it again to check idempotence, and runs tests against the result. It's the standard way to test roles you share.

Practice: make it clean, make it idempotent 🧪

A teammate wrote the web role in a hurry. Lint it, fix what it finds, prove it's idempotent, then run the smoke tests. They'll catch a problem lint can't.

Quick check

1. The second run of a playbook reports changed=1 on every server. What does that tell you?

2. ansible-lint passes and the playbook ran fine. Can the site still be broken?

3. Why is state: latest flagged?

Finished the missions and the quiz? Mark it done to track your progress.

Next up: AI Basics · Use AI wisely, starting with “AI as your Linux sidekick”.