Choose a purpose-built module
Modules such as copy, file, package, apt, service, and user describe desired state better than raw shell commands. command does not invoke a shell; shell is needed for pipes or redirection but can be non-idempotent and vulnerable when input is concatenated.
| Module | Typical job |
|---|---|
template |
Render a Jinja2 file with variables and copy it |
lineinfile |
Ensure one line exists in a file |
get_url |
Download a file |
git |
Check out a repository at a version |
systemd |
Manage a unit and run daemon-reload |
user, group |
Manage accounts |
ansible-doc ansible.builtin.template shows a module's options and examples without leaving the terminal.
flowchart LR
F["Facts<br/>ansible_os_family"] --> W{"when:<br/>Debian?"}
W -- "yes" --> APT["apt module<br/>state: present"]
W -- "no" --> DNF["dnf module<br/>state: present"]
APT --> REG["register: result<br/>changed?"]
REG --> LOOP["loop: users<br/>one item each"]
%% step F: Facts gathered from each host describe its OS, IPs and hardware.
%% step W: when decides per host whether a task runs.
%% step APT: Purpose-built modules like apt are idempotent: running twice changes nothing.
%% step DNF: Another branch handles Red Hat family hosts.
%% step REG: register saves a task's result so later tasks can use it.
%% step LOOP: loop repeats one task for each item in a list.
Variables and facts
Variables can come from inventory, group_vars, a play's vars: or vars_files:, a role's defaults, or the command line with -e "version=1.2", which has the highest precedence. Facts are variables Ansible gathers automatically; ansible web1 -m ansible.builtin.setup prints them. Common ones are ansible_facts.os_family, ansible_facts.distribution, and ansible_facts.default_ipv4.
Jinja2 filters transform values: {{ port | default(80) }} supplies a fallback, {{ name | upper }} changes case, and {{ items | length }} counts a list.
Loops, conditions, and results
- name: Install selected packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
when: ansible_facts.os_family == "Debian"
register: install_result
loop, when, and register are task keywords at the same level as the module call; they do not belong inside module arguments. When a YAML value begins with a Jinja expression, quote the entire value: name: "{{ package_name }}".
| Keyword | Purpose |
|---|---|
loop |
Run the task for each item |
when |
Run only when an expression is true |
register |
Save the result dictionary |
debug |
Display a variable or message |
changed_when / failed_when |
Decide what counts as changed or failed |
Facts have exact casing; RedHat differs from Redhat. The package module is more portable but does not expose every apt- or dnf-specific option.
Handling errors
A failed task stops that host by default. ignore_errors: true continues anyway and should be rare. A block with rescue and always sections works like try, catch, and finally: run steps, recover if one fails, and always clean up.