Skip to content

Ad-hoc commands & running playbooks

Two things build confidence with Ansible: running quick ad-hoc commands to inspect hosts, and previewing changes before you apply them. This page covers both, plus the everyday ways to run the playbooks. Everything runs from ~/ansible/ on the control node. For the full list of ready-made commands for this homelab, see the Quick Reference; when something fails, see Troubleshooting.

Ad-hoc commands

An ad-hoc command runs a single module against hosts without writing a playbook. Format:

ansible <target> -m <module> -a "<arguments>"
# Connectivity + sudo check (the 'ping' module — not ICMP)
ansible homelab -m ping

# Run a shell command (command module is the default, so -m is optional)
ansible server1.example.com -a "docker ps"
ansible homelab -a "uptime"

# Use a specific module
ansible server1.example.com -m service -a "name=docker state=restarted"
ansible server1.example.com -m ansible.builtin.apt -a "name=htop state=present"

# Print a variable
ansible server1.example.com -m debug -a "var=docker_compose_dir"

# Print facts (ad-hoc runs don't gather facts, so use the setup module)
ansible server1.example.com -m setup -a "filter=ansible_distribution_release"

<target> is any inventory pattern: a host (server1.example.com), a group (infra, homelab), or all.

-a commands always say CHANGED

The command/shell modules can't tell whether they changed anything, so ad-hoc shell commands report changed even when read-only. That's expected — see Writing Tasks → changed_when.

First time setup (test connectivity)

# Ping all hosts to verify Ansible can reach them
ansible homelab -m ping

# Expected output for each host:
# server1.example.com | SUCCESS => {
#     "changed": false,
#     "ping": "pong"
# }

If any host fails, check:

  • SSH key is copied to the host
  • Host is reachable (ssh -i ~/.ssh/ansible username@server1.example.com)
  • DNS resolves (.example.com hostname works)

More fixes: Troubleshooting.

Running playbooks

Playbooks are what you actually run (see Playbooks for what's in them).

# Provision base system on all hosts
ansible-playbook playbooks/base.yml

# Deploy stacks only
ansible-playbook playbooks/stacks.yml

# Run everything (base + stacks)
ansible-playbook playbooks/site.yml

# Run only against server1
ansible-playbook playbooks/site.yml --limit server1.example.com

# Run only against apps group
ansible-playbook playbooks/site.yml --limit apps

Check mode — the dry run

--check simulates a play: Ansible reports what would change but writes nothing. This is the single most useful habit for running with confidence.

# Dry run the base playbook against all hosts
ansible-playbook playbooks/base.yml --check

# Preview the whole base playbook against one host
ansible-playbook playbooks/base.yml --check --limit server1.example.com

Add --diff to see the exact before/after of every file a template or copy task would write:

ansible-playbook playbooks/base.yml --check --diff --limit server1.example.com

Check mode isn't perfect

Some tasks can't be simulated — e.g. a command that a later task depends on. Those may show as skipped or error under --check. It's a preview, not a guarantee, but it catches the vast majority of surprises.

Limiting scope — --limit

Never test on everything at once. Target one host first, then widen.

ansible-playbook playbooks/base.yml --limit server1.example.com   # one host
ansible-playbook playbooks/base.yml --limit infra             # one group
ansible-playbook playbooks/base.yml --limit '!nas.example.com'   # everything except nas

Tags — run part of a play

Add tags to your playbooks for selective execution:

# In playbooks/base.yml, add tags to each role:
roles:
  - { role: base, tags: ['base'] }
  - { role: docker, tags: ['docker'] }
  - { role: tailscale, tags: ['tailscale'] }
  - { role: mounts, tags: ['mounts'] }
  - { role: borgmatic, tags: ['borgmatic'] }

Then run or skip subsets:

# Only the docker role
ansible-playbook playbooks/base.yml --tags docker --limit server1.example.com

# Everything except borgmatic
ansible-playbook playbooks/base.yml --skip-tags borgmatic

# See which tags exist in a playbook
ansible-playbook playbooks/base.yml --list-tags

Look before you leap

# Which hosts would this run against?
ansible-playbook playbooks/stacks.yml --list-hosts

# Which tasks would run, in order?
ansible-playbook playbooks/base.yml --list-tasks

# Verbose output: -v (some), -vv (more), -vvv (SSH-level debug)
ansible-playbook playbooks/base.yml -v --limit server1.example.com

A safe workflow

graph LR
  A["--list-tasks"] --> B["--check --diff --limit one-host"]
  B --> C["run for real --limit one-host"]
  C --> D["run against the whole group"]
  1. --list-tasks — confirm what will run.
  2. --check --diff --limit <one host> — preview the changes.
  3. Run for real against that one host and verify it.
  4. Widen to the group once you're happy.

Ref: Ansible — Introduction to ad-hoc commands · Check mode · Tags