The Pieces: Control Node, Inventory, Modules, Plugins

Scenario: A new joiner opens the repository and finds ansible.cfg, an inventory folder, group_vars, a roles directory and a file called site.yml. They ask which of these Ansible actually reads, and in what order.

New words, in plain English

A working Ansible project has four moving parts.

The control node is where ansible-playbook runs. It holds the code, the credentials and the decision-making. Nothing about it is special - it is any machine with Ansible installed.

The inventory answers "which machines, and how do I reach them?" It can be a static file you write, or a script/plugin that asks a cloud API or a CMDB at run time.

Modules are the verbs. ansible.builtin.copy puts a file somewhere. ansible.builtin.service makes a service run. Each one is written to be idempotent - to check first and only act if reality differs.

Plugins and collections are the extension mechanism. Modules ship inside collections; ansible.builtin comes with Ansible itself, and everything else comes from Ansible Galaxy or your own repository.

Configuration comes from ansible.cfg, and Ansible looks for it in a strict order: the ANSIBLE_CONFIG environment variable, then ansible.cfg in the current directory, then ~/.ansible.cfg, then /etc/ansible/ansible.cfg. The first one found wins entirely - the files are not merged.

Analogy: Think of a delivery company. The control node is the depot. The inventory is the address book. Modules are the individual services the courier can perform - deliver, collect, get a signature. Collections are the catalogues those services are listed in. And ansible.cfg is the depot's standing instructions: which van to use, how many drops per route, whether to knock twice.

A worked example

# A conventional project layout. Ansible finds most of this by convention.
#
# ansible-project/
# ├── ansible.cfg              # settings: inventory path, ssh options, forks
# ├── site.yml                 # the entry-point playbook
# ├── inventory/
# │   └── hosts.ini            # which machines exist, and how to reach them
# ├── group_vars/
# │   ├── all.yml              # variables for every host
# │   └── webservers.yml       # variables for one group
# ├── host_vars/
# │   └── web01.yml            # variables for one machine
# └── roles/
#     └── webapp/              # reusable, packaged automation

Nothing here is magic - it is convention. Ansible looks for group_vars/ and host_vars/ next to your playbook (and next to your inventory), loads roles/ from roles_path, and reads ansible.cfg from the first location in the search order. Learning where each thing is looked up is most of what makes Ansible feel predictable rather than mysterious.

One detail that bites teams repeatedly: because the first ansible.cfg found wins outright, a stray ~/.ansible.cfg on one engineer's laptop can silently change behaviour for that person only. Keeping a project-local ansible.cfg and running from the project root is the reliable habit.

Tip: Run ansible-config dump --only-changed to see exactly which settings are non-default and where they came from. It answers 'why is it behaving like that?' faster than any amount of reading.
Goal: Put this to work in the ansible-playbook-syntax lab. Open /labs/ansible, pick ansible-playbook-syntax, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.