NovuSpark
All articles

August 22, 2025 · NovuSpark Team

Ansible 101: Inventory, Playbooks, and Ad-Hoc Commands

This is the first post in our Ansible fundamentals series. Later posts cover roles, variables and templates, Vault, and testing with Molecule.

Most configuration management tools require installing an agent on every machine they manage — a persistent process that phones home, needs its own updates, and is one more thing that can drift out of sync. Ansible's core design decision is refusing that trade: it manages remote machines entirely over SSH, using Python that's typically already present, and leaves nothing running once a task finishes.

That single decision explains most of what makes Ansible feel different to work with. This post covers the three concepts everything else in Ansible is built from.

Inventory: telling Ansible what "the fleet" means

An inventory is a list of the machines Ansible can manage, optionally organized into groups.

# inventory.ini
[web]
web1.internal ansible_host=10.0.1.10
web2.internal ansible_host=10.0.1.11
 
[db]
db1.internal ansible_host=10.0.2.10
 
[production:children]
web
db

Groups exist so you can target a subset of your fleet without repeating a list of hostnames everywhere. production:children above is a group of groups — anything targeting production automatically includes every host in web and db.

For anything beyond a handful of static hosts, a dynamic inventory — a script or plugin that queries AWS, Azure, or another source of truth at run time — replaces the static file entirely. The mental model stays identical either way: Ansible still just needs a list of hosts and groups; where that list comes from is the only thing that changes.

Ad-hoc commands: Ansible without writing a file first

Before playbooks, it's worth knowing Ansible can run a single task directly from the command line — useful for a quick check across a fleet, without writing any configuration at all:

ansible web -i inventory.ini -m ping
web1.internal | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web2.internal | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

-m ping invokes the ping module — not an ICMP ping, but a check that Ansible can connect and run Python on the target. Any module can be run this way:

ansible web -i inventory.ini -m shell -a "df -h /"

Ad-hoc commands are genuinely useful for exactly this kind of one-off, read-only check across a fleet. They're the wrong tool the moment you want the same set of actions to run reliably, repeatedly, and in a specific order — which is exactly what a playbook is for.

Playbooks: the actual unit of automation

A playbook is a YAML file describing a set of tasks to run against a set of hosts.

# webserver.yml
---
- name: Configure web servers
  hosts: web
  become: true
 
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
 
    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
 
    - name: Deploy index page
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
        owner: www-data
        group: www-data
        mode: "0644"
ansible-playbook -i inventory.ini webserver.yml

A few structural details worth being precise about:

  • hosts: web targets the web group from the inventory — this is the same targeting mechanism as ad-hoc commands, just declared in the file instead of on the command line.
  • become: true tells Ansible to escalate privileges (sudo, by default) for tasks that need it — installing packages, managing system services. It's set once at the play level rather than repeated per task.
  • Each task names a module (ansible.builtin.apt, ansible.builtin.service, ansible.builtin.copy) with parameters specific to that module. This is the core Ansible pattern: you describe what state a task should leave the system in (state: present, state: started), not the imperative commands to get there.

Idempotency: why running this twice should be safe

Run webserver.yml a second time, and Ansible's output distinguishes between tasks that actually changed something and tasks that found the system already in the desired state:

TASK [Install nginx] **********************************************
ok: [web1.internal]

TASK [Ensure nginx is running] ************************************
ok: [web1.internal]

ok (not changed) means Ansible checked, found nginx already installed and already running, and did nothing further. This property — safe to run repeatedly, with no effect beyond the first successful run — is called idempotency, and it's the property that makes playbooks trustworthy enough to run on a schedule, in CI, or as part of a deployment pipeline without fear of a second run doing something unexpected.

This doesn't happen automatically for every possible task — it's a property of well-written modules and well-written tasks. ansible.builtin.shell running an arbitrary command has no automatic idempotency at all; it's on you to make sure that command is safe to run more than once, or to guard it with a creates: or when: condition. We come back to this directly in the Molecule testing post later in this series.

What to actually remember from this post

  • Inventory is Ansible's list of hosts and groups — static for small, stable fleets; dynamic for anything sourced from a cloud provider.
  • Ad-hoc commands are for one-off checks; playbooks are for anything you want to run reliably, repeatedly.
  • Modules describe desired state, not imperative steps — that distinction is what makes idempotency possible in the first place.
  • ok vs. changed in playbook output is a feature, not noise — it's telling you exactly what Ansible actually did on this run.

Next in the series: Ansible Roles: Structuring Reusable Automation, where a single playbook file becomes a reusable, shareable unit.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.