Ansible, day one: idempotency, ad-hoc commands, and a first playbook
First real session with Ansible today, on the bastion host rather than the live server — reused it as a disposable lab instead of touching the box actually running the blog and honeypot.
What it actually is
Instead of SSHing in and typing commands by hand, you describe what state a server should be in, and Ansible does the SSHing and the deciding. It's agentless — nothing installs on the target, it only needs SSH access, which is why this worked immediately on a bastion I hadn't touched since setting it up. An inventory is the list of managed servers; a module is a prebuilt unit of work (yum, copy, shell, and thousands more); a playbook is a YAML file that strings several tasks together.
First command: ad-hoc, no file needed
ansible all -i inventory-lab.ini -m shell -a 'uptime'
-m picks the module, -a passes it an argument, -i points at the inventory file. One catch: shell always reports changed, even for a read-only command like this — it has no way to know whether anything actually changed, which turns out to matter a lot for the next part.
Idempotency, seen firsthand
Ran the exact same command twice, installing htop with the yum module:
ansible all -i inventory-lab.ini -m yum -a 'name=htop state=present' --become
First run: changed, with "installed": ["htop"] in the output — it wasn't there, so Ansible installed it. Second run, same exact command: changed: false, "already installed" — it checked first, saw nothing to do, and did nothing. That's the whole idea of idempotent modules: a playbook can be re-run any number of times without side effects, unlike a bash script that blindly re-runs mkdir and errors out the second time around.
A first playbook
Two tasks, YAML, spaces not tabs:
---
- name: My first playbook
hosts: lab_servers
become: true
tasks:
- name: Install htop
yum:
name: htop
state: present
- name: Create a test file in /tmp
copy:
content: "Hello from Ansible!\n"
dest: /tmp/hello.txt</code></pre>
Run with ansible-playbook instead of plain ansible:
ansible-playbook -i inventory-lab.ini my-first-playbook.yml
Output ends in a one-line PLAY RECAP that's worth reading before anything else: ok=3 changed=1 unreachable=0 failed=0 skipped=0. ok means the task succeeded and nothing needed to change; changed means it succeeded and actually did something; failed stops the play right there. Didn't take the recap's word for it either — ran cat /tmp/hello.txt on the box afterward to check the file was really there.
What I actually learned
- Agentless means the target needs nothing but SSH access — the bastion I built yesterday was ready to be a lab with zero setup.
shell reports changed unconditionally; purpose-built modules like yum actually check state first, which is the whole point of idempotency.
- Read the
PLAY RECAP line first — it's the entire playbook's result in one line.
- Trust but verify: Ansible said the file was created, so I checked it myself anyway.
Next: variables, handlers, loops, and roles — in that order, matching how the Ansible docs introduce them.