Ansible: idempotence + modules vs plugins vs collections¶
Mental model¶
Modules run on the target and enforce desired state (idempotent). Plugins run on the controller and extend Ansible's engine. Collections package both into namespaced, distributable bundles.
What it looks like¶
"Module" and "plugin" sound interchangeable. People use
command or shell for everything and wonder why Ansible
always reports "changed" even when nothing changed.
What it really is¶
- Idempotence -- running the same task twice produces the same end state. The second run changes nothing. This is the core contract of Ansible modules.
- Module -- a small program shipped to and executed on the target host. Manages one resource (user, file, package, service). Checks current state, compares to desired state, acts only if they differ.
- Plugin -- code that runs on the controller (your laptop or Ansible server). Types include: connection, lookup, filter, callback, inventory, strategy. Plugins extend how Ansible works, not what it configures.
- Collection -- a namespaced bundle of modules + plugins +
roles distributed via Ansible Galaxy or Automation Hub.
Example:
amazon.awscontainsamazon.aws.ec2_instance.
Why it seems confusing¶
- Both modules and plugins are Python code -- the distinction is WHERE they execute (target vs controller).
commandandshellare modules but they are not idempotent by default. They always report "changed" because Ansible cannot know if the command modified state.- The shift to collections (and FQCNs) added a naming layer that feels like bureaucracy but solves real namespace collisions.
What actually matters¶
- "changed" vs "ok" -- this is how Ansible reports whether it actually did something. Idempotent modules return "ok" on the second run. Non-idempotent ones always return "changed."
- Use purpose-built modules (
user,file,apt,service) instead ofshellwhenever possible -- you get idempotence, check mode, and diff mode for free. command/shellcan be made idempotent withcreates:orwhen:conditions, but it is manual work.- Use FQCNs (
ansible.builtin.copynotcopy) for clarity and to avoid collection name conflicts.
Common mistakes¶
- Using
shellto install packages instead ofapt/dnfmodule -- loses idempotence, check mode, and error handling - Ignoring "changed" status and treating every run as a fresh install
- Confusing lookup plugins (controller-side) with modules
(target-side) -- e.g.,
lookup('file', ...)reads a file on the controller,ansible.builtin.copywrites on target - Not using FQCNs -- works until two collections define a module with the same short name
Small examples¶
# Idempotent -- user module checks before acting
- name: Ensure deploy user exists
ansible.builtin.user:
name: deploy
state: present
# First run: changed (user created)
# Second run: ok (user already exists)
# NOT idempotent -- shell always reports changed
- name: Create directory (fragile)
ansible.builtin.shell: mkdir -p /opt/app
# Every run: changed (Ansible cannot tell)
# Better -- use file module
- name: Create directory (idempotent)
ansible.builtin.file:
path: /opt/app
state: directory
# First run: changed. Second run: ok.
# Collection FQCN usage
- name: Launch EC2 instance
amazon.aws.ec2_instance:
name: web-01
instance_type: t3.micro
image_id: ami-0abcdef1234567890
# Plugin vs module -- where they run
Controller (plugins): lookup, filter, callback, connection
Target (modules): user, file, apt, service, copy, template
One-line summary¶
Modules run on targets and enforce idempotent desired state; plugins run on the controller; collections bundle both with namespaced distribution.