Inventory and ad-hoc commands
Inventory in ini and yaml, host groups, ad-hoc commands with -m, the ping/command/shell/copy modules.
Why
Before Ansible can do anything, it needs to know which hosts to work on and how to connect to them. Inventory describes that. The second half of the lesson is about ad-hoc commands: one action on a group of machines without a playbook. People use them to check connectivity, look at the state of a fleet, and make one-off changes.
Inventory: the list of hosts
Inventory is a file listing hosts, their groups, and connection parameters.
The default path is /etc/ansible/hosts. In real projects the inventory
lives in the repository next to the playbook, and is passed with the -i
flag.
ini format
The most common and simplest option.
# inventory.ini
[web]
web-1.example.com
web-2.example.com ansible_user=deploy
[db]
db-1.example.com ansible_host=10.0.0.10 ansible_port=2222
[web:vars]
ansible_user=ubuntu
What's going on here:
[web]and[db]: host groups.ansible_host: the real IP or hostname, if the inventory host name doesn't resolve to the right address.ansible_user: which user to connect as over SSH. If not set, Ansible uses the name of the current user on the control node.ansible_port: the SSH port, if it isn't 22.[web:vars]: variables for the whole group.
Look at web-2.example.com. The web group sets the user to ubuntu,
but the host itself sets deploy. A host variable wins over a group
variable, so Ansible connects to web-2 as deploy, and to web-1 as
ubuntu.
The same inventory in yaml
# inventory.yml
all:
children:
web:
vars:
ansible_user: ubuntu
hosts:
web-1.example.com: {}
web-2.example.com:
ansible_user: deploy
db:
hosts:
db-1.example.com:
ansible_host: 10.0.0.10
ansible_port: 2222
Ansible reads both files into the same structure. yaml is convenient when the inventory is generated by a program, or has a lot of nested groups. ini is simpler to read and edit by hand.
Checking what Ansible parsed
Before running anything on hosts, it helps to see the inventory the way Ansible sees it:
ansible-inventory -i inventory.ini --graph
@all:
|--@ungrouped:
|--@web:
| |--web-1.example.com
| |--web-2.example.com
|--@db:
| |--db-1.example.com
The final variables for one host, with groups already accounted for:
ansible-inventory -i inventory.ini --host web-2.example.com
{
"ansible_user": "deploy"
}
Special groups
Ansible always adds two groups on its own:
| Group | What it includes |
|---|---|
all | every host in the inventory |
ungrouped | hosts that don't fall into any explicit group |
The all pattern in a command means exactly this group.
Ad-hoc commands
Ad-hoc is a one-off action without a playbook. Good for checking connectivity, quickly running a command on a fleet of machines, or making a one-time change.
Syntax:
ansible <pattern> -i inventory.ini -m <module> -a "<args>"
The pattern picks which hosts to work on:
all: every host;web: the web group;web-1.example.com: a specific host;web,db: several groups;'all:!db': everything except the db group.
Put a pattern with ! in single quotes. In an interactive bash shell, an
exclamation mark triggers history expansion, and without quotes the
command breaks.
If -m isn't given, Ansible uses the command module. So
ansible web -a "uptime" and ansible web -m command -a "uptime" do the
same thing.
Everything happens through modules
Ansible almost never sends a raw command to a host. It calls modules:
small programs that get copied to the managed node and run there by
Python. Each module does one thing and reports whether it changed
anything (CHANGED) or everything was already fine (SUCCESS).
Basic modules:
| Module | What it does |
|---|---|
ping | Checks SSH access and a working Python on the host. Not ICMP. |
command | Runs a command without a shell. |
shell | Runs a command through /bin/sh: pipes and redirects work. |
copy | Copies a file from the control node to the managed node. |
apt | Manages packages on Debian/Ubuntu. |
dnf | The same for RHEL/Rocky/AlmaLinux/Fedora. The old name yum is redirected to dnf in ansible-core. |
service | Manages services through any init system (systemd, OpenRC, SysV). There's a separate systemd module for systemd specifics (daemon-reload, masks). |
user | Creates and modifies users. |
command and shell
command runs a program directly, without a shell. Pipes (|), redirects
(>), ;, and && don't work: they're passed to the program as plain
arguments. The module expands $HOME-style environment variables itself,
without a shell.
shell is for when you can't do without a shell: pipes
(ps aux | grep nginx), redirects (> /tmp/out), globs (*.conf). But if
user input ends up in the arguments, there's a risk of injection.
Rule: command by default, shell only when you need shell features.
Ad-hoc command examples
Check connectivity with every host:
ansible all -i inventory.ini -m ping
Get the uptime on the web group:
ansible web -i inventory.ini -m command -a "uptime"
Install a package with escalated privileges:
ansible all -i inventory.ini -m apt -a "name=nginx state=present" -b
Copy a file to every web server:
ansible web -i inventory.ini -m copy -a "src=./nginx.conf dest=/etc/nginx/nginx.conf" -b
Restart a service:
ansible web -i inventory.ini -m service -a "name=nginx state=restarted" -b
become: privilege escalation
Many operations need root: installing packages, writing to /etc,
managing services. The -b flag (or --become) tells Ansible to run the
module through sudo.
ansible all -i inventory.ini -m apt -a "name=vim state=present" -b
By default, become uses sudo. Another mechanism (su, doas, and so on) is
set with the --become-method flag. If sudo on the host asks for a
password, add -K: Ansible will ask for it once before running.
In a playbook, become is set at the play level or on an individual task. That's more convenient than remembering the flag for every ad-hoc command.
Common mistakes
- UNREACHABLE instead of SUCCESS. Ansible couldn't log in over SSH.
Read the text after
UNREACHABLE!:Could not resolve hostnamemeans the name doesn't resolve (checkansible_host),Connection refusedmeans nobody's listening on that port (checkansible_port),Permission deniedmeans the wrong user or key (checkansible_user). - A group variable didn't apply to one host. The host has its own
variable with the same name, and it wins. Check with
ansible-inventory --host <name>. - A pipe in
command.ansible web -a "ps aux | grep nginx"won't filter the output:|gets passed topsas an argument. You need-m shell. - Forgotten
-b. Writing to a system directory fails with a permissions error (Permission denied,not writable). Ansible isn't at fault here: it just doesn't have root. - A pattern with
!without single quotes. bash printsevent not found.
What you'll need for the tasks
- ini format:
[name]groups, host variables on the same line,[group:vars]; - yaml format:
all→children→ group →hostsandvars; ansible_host,ansible_port,ansible_user;ansible-inventory -i <file> --graphand--host <name>;ansible <pattern> -i <file> -m ping;-m copy -a "src=... dest=..."and the-bflag.
Self-check
Type the command or key line. Case and spaces don't matter.
Interview-style questions
Interview questions on this topic: 3
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson ansible-basics-02-inventory-adhoc
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress