Writing Idempotent Ansible Playbooks with Honest Check Mode

Idempotency Is an Availability Property

An idempotent playbook describes the desired state and reports changed only when it moves a host toward that state. Re-running it should be safe. This matters because operators retry after partial failures, pipelines reconcile regularly, and emergency runs happen under pressure.

Prefer a module that understands the resource:

- name: Create the application directory
  ansible.builtin.file:
    path: /srv/catalog
    state: directory
    owner: catalog
    group: catalog
    mode: "0750"

- name: Set the listening port once
  ansible.builtin.lineinfile:
    path: /srv/catalog/config.ini
    regexp: '^listen_port\s*='
    line: 'listen_port = 8080'
    create: true
    mode: "0644"

Compare that with shell: mkdir ... and echo ... >> config.ini. The shell cannot infer desired state, append duplicates accumulate, and every run may look changed.

---

When a Command Is Legitimate

Some vendor tools have no module. Give Ansible enough information to report honestly:

- name: Build the search index once
  ansible.builtin.command: /opt/catalog/bin/build-index
  args:
    creates: /srv/catalog/index.db

- name: Check application drift
  ansible.builtin.command: /opt/catalog/bin/drift-check
  register: drift
  changed_when: "'DRIFT' in drift.stdout"
  failed_when: drift.rc not in [0, 2]

Do not set changed_when: false merely to silence noisy output. That can suppress a handler that should restart a service after real change.

Understand Check Mode's Limits

--check predicts changes for modules that support it. It does not create a temporary future state, so a later task may fail because a file an earlier task would have created is still absent. Commands and external APIs may not support prediction at all.

Use --check --diff as review evidence, not proof of correctness. Mark unsupported tasks explicitly, avoid real writes during dry runs, and verify on an ephemeral target.

The Two-Run Test

CI should run the playbook once, run it again, and assert the second recap reports zero changes. Then verify the application state independently. This catches duplicate lines, dishonest commands, templates containing volatile timestamps, and handlers firing every run.

Apply the workflow in Stop the Duplicate Config, Honest Command Semantics, and Make Check Mode Safe.